/*******************************************************************************
* Copyright (C) 2010 Christian Bockermann <chris@jwall.org>
*
* This file is part of the jwall-rbld program. jwall-rbld is an implementation
* of a simple DNS server for running a local, customized real time block-list.
* More information and documentation for the jwall-rbld can be found at
*
* http://www.jwall.org/jwall-rbld
*
* 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; either version 3 of the License, or (at your option) any later version.
*
* 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/>.
******************************************************************************/
package org.jwall.rbl;
import java.io.File;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.Properties;
import org.jwall.rbl.data.RBList;
import org.jwall.rbl.data.RBListEntry;
import org.jwall.rbl.data.RblFile;
import org.jwall.rbl.data.RblSettings;
import org.jwall.rbl.dns.Query;
import org.jwall.rbl.dns.QueryHandler;
import org.jwall.rbl.dns.RblSecurityManager;
import org.jwall.rbl.dns.Response;
import org.jwall.rbl.net.AdminHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* This class implements a very simple DNS server for running a
* real-time blackhole list. The server only responds to A records.
* Support for AAAA record queries is planned.
* </p>
*
* @author Christian Bockermann <chris@jwall.org>
*
*/
public class RblServer
extends Thread
{
/* The global logger for this class */
public static Logger log = LoggerFactory.getLogger( RblServer.class );
public final static String RBL_HOME = "RBL_HOME";
public final static String RBL_PORT = "rbl.port";
public final static String RBL_ADDRESS = "rbl.address";
public final static String RBL_ADMIN_PORT = "rbl.admin.port";
public final static String RBL_FILE = "rbl.file";
public final static String RBL_DOMAIN = "rbl.domain";
public final static String RBL_PERMISSIONS = "rbl.permission.file";
/** These properties can be overwritten using system properties (command line) */
public final static String[] PROPERTY_NAMES = {
RBL_PORT, RBL_ADDRESS, RBL_ADMIN_PORT, RBL_FILE, RBL_DOMAIN, RBL_PERMISSIONS
};
public static Inet4Address BLOCKED_VALUE = null;
public final static String VERSION = "v0.1";
static {
try {
BLOCKED_VALUE = (Inet4Address) InetAddress.getByName( "127.0.0.1" );
} catch (Exception e){}
}
/** The block list which is served by this server */
RBList rbl;
/** The DNS domain name which this server handles queries for */
String domain = "rbl.localnet";
/** The UDP server socket for receiving queries */
DatagramSocket socket;
long queryCount = 0L;
boolean running = true;
int adminPort = -1;
AdminHandler adminInterface;
/** This is the query handler, which creates responses for queries */
QueryHandler queryHandler;
/** This is a reference to the security manager, validating block/unblock request on IP basis... */
RblSecurityManager securityManager;
/**
* Create a new RblServer with the provided set of properties.
*
* @param p
* @throws Exception
*/
public RblServer( Properties p ) throws Exception {
String addr = "127.0.0.1";
Integer port = 15353;
File rblFile = new File( File.separator + "var" + File.separator + "lib" + File.separator + "jwall-rbl" + File.separator + "local.rbl" );
try {
if( p.getProperty( "rbl.port" ) != null )
port = new Integer( p.getProperty( "rbl.port" ) );
} catch (Exception e) {
throw new Exception( "Invalid port '" + p.getProperty( "rbl.port" ) + "' specified!" );
}
try {
if( p.getProperty( "rbl.address" ) != null )
addr = InetAddress.getByName( p.getProperty( "rbl.address" ) ).getHostAddress();
} catch (Exception e) {
throw new Exception( "Failed to set address '" + p.getProperty( "rbl.address" ) + "'!" );
}
if( p.getProperty( RBL_DOMAIN ) != null )
domain = p.getProperty( RBL_DOMAIN );
try {
if( p.getProperty( "rbl.admin.port" ) != null )
adminPort = new Integer( p.getProperty( "rbl.admin.port" ) );
} catch (Exception e) {
}
try {
if( p.getProperty( "rbl.file" ) != null )
rblFile = new File( p.getProperty( "rbl.file" ) );
rbl = new RblFile( rblFile );
} catch (Exception e) {
throw new Exception( "Failed to read rbl-list from file '" + "': " + e.getMessage() );
}
File policyFile = new File( File.separator + "etc" + File.separator + "jwall-rbld.permissions" );
try {
if( p.getProperty( RBL_PERMISSIONS ) != null )
policyFile = new File( p.getProperty( RBL_PERMISSIONS ) );
if( policyFile.isFile() )
RblSecurityManager.getInstance().readPermissions( policyFile );
else
log.debug( "Permission file {} does not exist, updates of DNS is disabled.", policyFile.getAbsolutePath() );
} catch (Exception e) {
throw new Exception( "Failed to load permissions from '" + policyFile.getAbsolutePath() + "': " + e.getMessage() );
}
log.debug( "Starting jwall-rbld version {} for domain '{}'", VERSION, getDomain() );
log.debug( "Listening on UDP port {}:{}", addr, port );
queryHandler = new QueryHandler( this );
socket = new DatagramSocket( port, InetAddress.getByName( addr ) );
/*
DatagramChannel channel = DatagramChannel.open();
socket = channel.socket();
channel.configureBlocking( true );
socket.connect( InetAddress.getByName( addr ), port );
*/
}
/**
* This method creates a new RBL server listening at the specified address
* on the given port.
*
* @param addr
* @param port
* @throws Exception
*/
public RblServer( InetAddress addr, Integer port ) throws Exception {
log.debug( "Creating RblServer listening at {}, port {}", addr.getHostAddress(), port );
queryHandler = new QueryHandler( this );
/*
DatagramChannel channel = DatagramChannel.open();
socket = channel.socket();
channel.configureBlocking( true );
socket.connect( addr, port );
*/
socket = new DatagramSocket( port, addr );
}
public void setRblSecurityManager( RblSecurityManager manager ){
securityManager = manager;
}
public RblSecurityManager getRblSecurityManager(){
if( this.securityManager != null )
return securityManager;
return RblSecurityManager.getInstance();
}
/**
* Sets the block list for this server.
*
* @param list
*/
public void setBlockList( RBList list ){
this.rbl = list;
}
public RBList getBlockList(){
return this.rbl;
}
public String getDomain(){
return this.domain;
}
public void block( String address, Integer ttl ){
String key = QueryHandler.getKeyForAddress( address, getDomain() );
log.debug( "Adding rbl-entry with key = '{}'", key );
RBListEntry entry = new RBListEntry( null, key );
entry.setName( address );
entry.setCreated( System.currentTimeMillis() );
entry.setLifetime( ttl );
getBlockList().add(entry);
}
public void unblock( String address ){
String key = QueryHandler.getKeyForAddress( address, getDomain() );
log.debug( "Removing rbl-entry with key = '{}'", key );
getBlockList().remove( key );
}
/**
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
if( adminPort > 0 ){
log.debug( "Starting admin interface on port {}", adminPort );
adminInterface = new AdminHandler( this, "localhost", adminPort );
adminInterface.start();
} else
log.debug( "No admin-port defined, not starting admin interface!" );
} catch (Exception e){
e.printStackTrace();
}
try {
socket.setSoTimeout( 1000 );
} catch (Exception e) {}
while( running ){
try {
DatagramPacket packet = new DatagramPacket( new byte[1024], 1024 );
log.trace( "Waiting for incoming DNS queries..." );
socket.receive(packet);
log.debug( "Received packet {}... size is {}", packet.getAddress() + ":" + packet.getPort(), packet.getLength() );
byte[] data = packet.getData();
log.debug( "Received packet of size {} bytes: {}", data.length, new String(data) );
Query q = Query.parse( data, 0 );
Response response = queryHandler.process( packet.getAddress(), q );
byte[] re = response.toByteArray();
DatagramPacket answer = new DatagramPacket( re, 0 );
answer.setLength( re.length );
answer.setAddress( packet.getAddress() );
answer.setPort( packet.getPort() );
socket.send( answer );
queryCount++;
} catch (SocketTimeoutException ste){
} catch (Exception e) {
e.printStackTrace();
}
}
log.debug( "Closing UDP socket..." );
socket.close();
}
/**
* This method is called upon server shutdown, e.g. by the
* VM's ShutdownHook. It will save the
*
*/
public void shutdown(){
log.info( "Received shutdown signal!" );
rbl.store();
// if there is an admin handler running, we need to stop
// that one as well
//
if( adminInterface != null ){
adminInterface.shutdown();
try {
adminInterface.join( 1000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info( "Setting running=false" );
running = false;
log.info( "Sending interrupt() signal..." );
interrupt();
log.info( "Disconnecting socket..." );
socket.disconnect();
}
/**
* <p>
* This is the server's main entry point. It will read the configuration from
* <code>${RBL_HOME}/etc/jwall-rbld.conf</code> if that file exists or from
* a configuration file specified as first argument at the command line.
* </p>
*
* @param args
* @throws Exception
*/
public static void main( String[] args ) throws Exception {
String base = System.getenv( RBL_HOME );
if( base == null )
base = "";
String config = base + File.separator + "etc" + File.separator + "jwall-rbld.conf";
if( args.length > 0 )
config = args[0];
else {
String[] etcs = new String[]{ base + "/etc/jwall-rbld.conf", "/etc/jwall-rbld.conf", "/opt/modsecurity/etc/jwall-rbld.conf" };
for( String etc : etcs ){
File ef = new File( etc );
if( ef.exists() ){
log.info( "Reading configuration from {}", ef.getAbsolutePath() );
config = ef.getAbsolutePath();
break;
}
}
}
final RblSettings p = RblSettings.read( config );
final RblServer server = new RblServer( p );
server.start();
//
// register a shutdown hook which will fire as this process
// receives a TERM signal
//
Runtime.getRuntime().addShutdownHook( new Thread(){
public void run(){
server.shutdown();
}
});
}
}