/*************************************************************************
* Copyright 2009-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2008, Regents of the University of California
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
* THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
* COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
* AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
* IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
* SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
* WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
* REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
* IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
* NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
************************************************************************/
package com.eucalyptus.cloud.ws;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.log4j.Logger;
import com.eucalyptus.system.Capabilities;
import org.jboss.netty.bootstrap.ConnectionlessBootstrap;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.FixedReceiveBufferSizePredictor;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.DatagramChannelFactory;
import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
import org.jboss.netty.channel.socket.nio.NioDatagramChannelFactory;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.frame.LengthFieldBasedFrameDecoder;
import org.jboss.netty.handler.codec.frame.LengthFieldPrepender;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.xbill.DNS.ResolverConfig;
import com.eucalyptus.system.Threads;
import com.google.common.base.Strings;
import com.eucalyptus.configurable.*;
import com.eucalyptus.util.Cidr;
import com.eucalyptus.util.DNSProperties;
import com.eucalyptus.util.Internets;
import com.eucalyptus.util.LockResource;
import com.eucalyptus.ws.WebServices;
import com.google.common.base.CharMatcher;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@ConfigurableClass( root = "dns", description = "Controls dns listeners." )
public class DNSControl {
private static Logger LOG = Logger.getLogger( DNSControl.class );
private static final AtomicReference<Collection<Cidr>> addressMatchers =
new AtomicReference<Collection<Cidr>>( Collections.<Cidr>emptySet( ) );
private static final AtomicReference<Collection<TCPListener>> tcpListenerRef =
new AtomicReference<Collection<TCPListener>>( Collections.<TCPListener>emptySet( ) );
private static final Lock listenerLock = new ReentrantLock( );
@ConfigurableField( displayName = "dns_listener_address_match",
description = "Additional address patterns to listen on for DNS requests.",
initial = "",
readonly = false,
changeListener = DnsAddressChangeListener.class)
public static volatile String dns_listener_address_match = "";
@ConfigurableField( description = "Server worker thread pool max.",
initial = "512",
changeListener = WebServices.CheckNonNegativeIntegerPropertyChangeListener.class )
public static Integer SERVER_POOL_MAX_THREADS = 512;
@ConfigurableField( displayName = "server",
description = "Comma separated list of nameservers, OS settings used if none specified (change requires restart)",
initial = "",
readonly = false,
changeListener = DnsServerChangeListener.class)
public static volatile String server = "";
@ConfigurableField( displayName = "search",
description = "Comma separated list of domains to search, OS settings used if none specified (change requires restart)",
initial = "",
readonly = false,
changeListener = DnsSearchChangeListener.class)
public static volatile String search = "";
public static class DnsServerChangeListener implements PropertyChangeListener {
@Override
public void fireChange( ConfigurableProperty t, Object newValue ) throws ConfigurablePropertyException {
if (newValue == null || (newValue instanceof String)) {
try {
String newValueStr = (String) newValue;
if (Strings.isNullOrEmpty(newValueStr)) {
LOG.debug("Setting dns.server property to null (by clearing it)");
System.clearProperty("dns.server");
} else {
LOG.debug("Setting dns.server property to " + newValueStr);
System.setProperty("dns.server", newValueStr);
}
ResolverConfig.refresh();
} catch ( final Exception e ) {
throw new ConfigurablePropertyException( e.getMessage( ) );
}
}
}
}
public static class DnsSearchChangeListener implements PropertyChangeListener {
@Override
public void fireChange( ConfigurableProperty t, Object newValue ) throws ConfigurablePropertyException {
if (newValue == null || (newValue instanceof String)) {
try {
String newValueStr = (String) newValue;
if (Strings.isNullOrEmpty(newValueStr)) {
LOG.debug("Setting dns.search property to null (by clearing it)");
System.clearProperty("dns.search");
} else {
LOG.debug("Setting dns.search property to " + newValueStr);
System.setProperty("dns.search", newValueStr);
}
ResolverConfig.refresh();
} catch ( final Exception e ) {
throw new ConfigurablePropertyException( e.getMessage( ) );
}
}
}
}
public static class DnsAddressChangeListener implements PropertyChangeListener {
@Override
public void fireChange( ConfigurableProperty t, Object newValue ) throws ConfigurablePropertyException {
if ( newValue instanceof String ) {
updateAddressMatchers( (String) newValue );
}
}
}
private static void updateAddressMatchers( final String addressCidrs ) throws ConfigurablePropertyException {
try {
addressMatchers.set( ImmutableList.copyOf( Iterables.transform(
Splitter.on( CharMatcher.anyOf(", ;:") ).trimResults( ).omitEmptyStrings( ).split( addressCidrs ),
Cidr.parseUnsafe( )
) ) );
} catch ( IllegalArgumentException e ) {
throw new ConfigurablePropertyException( e.getMessage( ) );
}
}
private static final ChannelGroup udpChannelGroup = new DefaultChannelGroup(
DNSControl.class.getSimpleName( )
+ ":udp:53");
private static final ChannelGroup tcpChannelGroup = new DefaultChannelGroup(
DNSControl.class.getSimpleName( )
+ ":tcp:53");
private static DatagramChannelFactory udpChannelFactory = null;
private static ServerSocketChannelFactory tcpChannelFactory = null;
private static ExecutionHandler udpExecHandler = null;
private static Executor createWorkerPool( String name ) {
return Executors.newFixedThreadPool(
SERVER_POOL_MAX_THREADS,
Threads.threadFactory( "dns-" + name + "-worker-pool-%d" ) );
}
public static class TimedDns {
private Long receivedTime;
private byte[] request;
public Long getReceivedTime(){
return receivedTime;
}
public byte[] getRequest(){
return request;
}
}
private static class DnsTimestampWrapper implements ChannelUpstreamHandler {
@Override
public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent ce)
throws Exception {
if (!(ce instanceof MessageEvent)) {
return;
}
try {
final MessageEvent me = (MessageEvent) ce;
final ChannelBuffer buffer = ((ChannelBuffer) me.getMessage());
final TimedDns wrappedRequest = new TimedDns();
wrappedRequest.receivedTime = (new Date()).getTime();
wrappedRequest.request = new byte[buffer.readableBytes( )];
buffer.getBytes(0, wrappedRequest.request);
Channels.fireMessageReceived(ctx, wrappedRequest,
((InetSocketAddress) me.getRemoteAddress()));
}catch(final Exception ex) {
LOG.debug(ex,ex);
}
}
}
private static class UdpChannelPipelineFactory implements ChannelPipelineFactory {
private ExecutionHandler execHandler = null;
private UdpChannelPipelineFactory(final ExecutionHandler execHandler) {
this.execHandler = execHandler;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new DnsTimestampWrapper(), this.execHandler, new DnsServerHandler());
}
}
private static void initializeUDP( ) throws Exception{
if(udpChannelFactory == null){
try{
udpChannelFactory = new NioDatagramChannelFactory(
Executors.newCachedThreadPool( Threads.threadFactory( "dns-server-udp-pool-%d" ) ) );
final ConnectionlessBootstrap b = new ConnectionlessBootstrap(udpChannelFactory);
udpExecHandler =
new ExecutionHandler(createWorkerPool( "udp" ));
b.setPipelineFactory(new UdpChannelPipelineFactory(udpExecHandler));
b.setOption("receiveBufferSize", 4194304);
b.setOption("broadcast", "false");
b.setOption("receiveBufferSizePredictor", new FixedReceiveBufferSizePredictor(1024));
b.setOption( "child.tcpNoDelay", true );
b.setOption( "child.reuseAddress", true);
b.setOption( "child.connectTimeoutMillis", 3000 );
b.setOption("tcpNoDelay", true);
b.setOption("reuseAddress", true);
b.setOption("connectTimeoutMillis", 3000);
final Set<InetAddress> listenAddresses = Sets.newLinkedHashSet( );
listenAddresses.add( Internets.localHostInetAddress( ) );
if(addressMatchers.get().size()>0) {
Iterables.addAll(
listenAddresses,
Iterables.filter( Internets.getAllInetAddresses( ), Predicates.or( addressMatchers.get( ) ) ) );
}else{
Iterables.addAll(
listenAddresses,
Internets.getAllInetAddresses( ));
}
Capabilities.runWithCapabilities( new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Channel udpChannel= null;
for(final InetAddress listenAddr : listenAddresses) {
try{
udpChannel = b.bind(new InetSocketAddress(listenAddr, 53));
udpChannelGroup.add(udpChannel);
}catch(final Exception ex){
continue;
}
}
return true;
}});
}catch(final Exception ex) {
LOG.debug("Failed initializing DNS udp listener",ex);
udpChannelGroup.close().awaitUninterruptibly();
if(udpChannelFactory!=null) {
udpChannelFactory.releaseExternalResources();
udpChannelFactory = null;
}
if(udpExecHandler!=null){
udpExecHandler.releaseExternalResources();
udpExecHandler = null;
}
throw ex;
}
}
}
private static void initializeTCP() throws Exception{
initializeTCPSocket();
}
private static void initializeTCPSocket( ) throws Exception{
initializeListeners( tcpListenerRef, "TCP", new ListenerBuilder<TCPListener>() {
@Override
public TCPListener build( final InetAddress address, final int port ) throws IOException {
return new TCPListener( address, port );
}
});
}
private static void initializeTCPNetty() throws Exception {
// currently there's an issue with bind() throwing Socket permission exception
// due to the port < 1024
if(tcpChannelFactory == null){
try{
tcpChannelFactory =
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool( Threads.threadFactory( "dns-server-tcp-pool-%d" ) ),
createWorkerPool("tcp"));
final ServerBootstrap b = new ServerBootstrap(tcpChannelFactory);
b.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline p = Channels.pipeline();
p.addLast("framer", new LengthFieldBasedFrameDecoder(65536, 0, 2, 0, 2));
p.addLast("prepender", new LengthFieldPrepender(2));
p.addLast("dns-server", new DnsServerHandler());
return p;
}
});
b.setOption( "child.tcpNoDelay", true );
b.setOption( "child.keepAlive", false );
b.setOption( "child.reuseAddress", true );
b.setOption( "child.connectTimeoutMillis", 3000 );
b.setOption("tcpNoDelay", true);
b.setOption("keepAlive", false);
b.setOption("reuseAddress", true);
b.setOption("connectTimeoutMillis", 3000);
Capabilities.runWithCapabilities( new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
final Channel tcpChannel = b.bind(new InetSocketAddress(53));
tcpChannelGroup.add(tcpChannel);
return true;
}
});
}catch(final Exception ex) {
LOG.debug(ex,ex);
tcpChannelGroup.close().awaitUninterruptibly();
if(tcpChannelFactory!=null)
tcpChannelFactory.releaseExternalResources();
tcpChannelFactory = null;
throw ex;
}
}
}
private static <T extends Thread> void initializeListeners(
final AtomicReference<Collection<T>> listenerRef,
final String description,
final ListenerBuilder<T> builder
) {
try ( final LockResource lock = LockResource.lock( listenerLock ) ) {
if ( listenerRef.get( ).isEmpty( ) ) {
final int listenPort = DNSProperties.PORT;
final Set<InetAddress> listenAddresses = Sets.newLinkedHashSet( );
listenAddresses.add( Internets.localHostInetAddress( ) );
if(addressMatchers.get().size()>0) {
Iterables.addAll(
listenAddresses,
Iterables.filter( Internets.getAllInetAddresses( ), Predicates.or( addressMatchers.get( ) ) ) );
}else{
Iterables.addAll(
listenAddresses,
Internets.getAllInetAddresses( ));
}
LOG.info( "Starting DNS " + description + " listeners on " + listenAddresses + ":" + listenPort );
// Configured listeners
final List<T> listeners = Lists.newArrayList( );
for ( final InetAddress listenAddress : listenAddresses ) {
try {
final T listener = Capabilities.runWithCapabilities( new Callable<T>() {
@Override
public T call() throws Exception {
final T listener = builder.build( listenAddress, listenPort );
listener.start( );
return listener;
}
} );
listeners.add( listener );
} catch( final Exception ex ) {
LOG.error( "Error starting DNS "+description+" listener on "+listenAddress+":"+listenPort, ex );
}
}
listenerRef.set( ImmutableList.copyOf( listeners ) );
}
}
}
private interface ListenerBuilder<T> {
T build( InetAddress address, int port ) throws IOException;
}
public static void initialize() throws Exception {
try {
initializeUDP();
initializeTCP();
} catch(Exception ex) {
LOG.error("DNS could not be initialized. Is some other service running on port 53?", ex);
throw ex;
}
}
public static void stop() throws Exception {
if(udpChannelGroup!=null)
udpChannelGroup.close( ).awaitUninterruptibly();
if(udpExecHandler!=null) {
udpExecHandler.releaseExternalResources();
udpExecHandler = null;
}
if(udpChannelFactory!=null) {
udpChannelFactory.releaseExternalResources( );
udpChannelFactory = null;
}
/* if(tcpChannelGroup!=null)
tcpChannelGroup.close().awaitUninterruptibly();
if(tcpChannelFactory!=null) {
tcpChannelFactory.releaseExternalResources();
tcpChannelFactory = null;
}*/
}
public static void restart() throws Exception {
stop();
initialize();
}
public DNSControl() {}
}