/*
* Copyright 2009-2014 Jagornet Technologies, LLC. All Rights Reserved.
*
* This software is the proprietary information of Jagornet Technologies, LLC.
* Use is subject to license terms.
*
*/
/*
* This file BaseDhcpV4Processor.java is part of Jagornet DHCP.
*
* Jagornet DHCP 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.
*
* Jagornet DHCP 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 Jagornet DHCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.jagornet.dhcp.server.request;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jagornet.dhcp.message.DhcpV4Message;
import com.jagornet.dhcp.option.base.DhcpOption;
import com.jagornet.dhcp.option.v4.DhcpV4ClientFqdnOption;
import com.jagornet.dhcp.option.v4.DhcpV4HostnameOption;
import com.jagornet.dhcp.option.v4.DhcpV4LeaseTimeOption;
import com.jagornet.dhcp.option.v4.DhcpV4RequestedIpAddressOption;
import com.jagornet.dhcp.option.v4.DhcpV4ServerIdOption;
import com.jagornet.dhcp.server.config.DhcpConfigObject;
import com.jagornet.dhcp.server.config.DhcpLink;
import com.jagornet.dhcp.server.config.DhcpServerConfiguration;
import com.jagornet.dhcp.server.config.DhcpServerPolicies;
import com.jagornet.dhcp.server.config.DhcpV4OptionConfigObject;
import com.jagornet.dhcp.server.config.DhcpServerPolicies.Property;
import com.jagornet.dhcp.server.request.binding.Binding;
import com.jagornet.dhcp.server.request.binding.BindingObject;
import com.jagornet.dhcp.server.request.binding.V4BindingAddress;
import com.jagornet.dhcp.server.request.ddns.DdnsCallback;
import com.jagornet.dhcp.server.request.ddns.DdnsUpdater;
import com.jagornet.dhcp.server.request.ddns.DhcpV4DdnsComplete;
import com.jagornet.dhcp.util.DhcpConstants;
import com.jagornet.dhcp.util.Util;
/**
* Title: BaseDhcpV4Processor
* Description: The base class for processing DHCPv4 client messages.
*
* @author A. Gregory Rabil
*/
public abstract class BaseDhcpV4Processor implements DhcpV4MessageProcessor
{
private static Logger log = LoggerFactory.getLogger(BaseDhcpV4Processor.class);
protected static DhcpServerConfiguration dhcpServerConfig =
DhcpServerConfiguration.getInstance();
// wrap the configured V4ServerId option in a DhcpOption for the wire
protected static DhcpV4ServerIdOption dhcpV4ServerIdOption =
new DhcpV4ServerIdOption(dhcpServerConfig.getDhcpServerConfig().getV4ServerIdOption());
protected final DhcpV4Message requestMsg;
protected DhcpV4Message replyMsg;
protected final InetAddress clientLinkAddress;
protected DhcpLink clientLink;
protected List<Binding> bindings = new ArrayList<Binding>();
protected static Set<DhcpV4Message> recentMsgs =
Collections.synchronizedSet(new HashSet<DhcpV4Message>());
protected static Timer recentMsgPruner = new Timer("RecentMsgPruner");
/**
* Construct an BaseDhcpRequest processor. Since this class is
* abstract, this constructor is protected for implementing classes.
*
* @param requestMsg the DhcpMessage received from the client
* @param clientLinkAddress the client link address
*/
protected BaseDhcpV4Processor(DhcpV4Message requestMsg, InetAddress clientLinkAddress)
{
this.requestMsg = requestMsg;
this.clientLinkAddress = clientLinkAddress;
}
protected Map<Integer, DhcpOption> requestedOptions(Map<Integer, DhcpOption> optionMap,
DhcpV4Message requestMsg)
{
if ((optionMap != null) && !optionMap.isEmpty()) {
List<Integer> requestedCodes = requestMsg.getRequestedOptionCodes();
if ((requestedCodes != null) && !requestedCodes.isEmpty()) {
Map<Integer, DhcpOption> _optionMap = new HashMap<Integer, DhcpOption>();
for (Map.Entry<Integer, DhcpOption> option : optionMap.entrySet()) {
if (requestedCodes.contains(option.getKey())) {
_optionMap.put(option.getKey(), option.getValue());
}
}
optionMap = _optionMap;
}
}
return optionMap;
}
/**
* Populate v4 options.
*
* @param link the link
* @param configObj the config object or null if none
*/
protected void populateV4Reply(DhcpLink dhcpLink, DhcpV4OptionConfigObject configObj)
{
String sname = DhcpServerPolicies.effectivePolicy(requestMsg, configObj,
dhcpLink.getLink(), Property.V4_HEADER_SNAME);
if ((sname != null) && !sname.isEmpty()) {
replyMsg.setsName(sname);
}
String filename = DhcpServerPolicies.effectivePolicy(requestMsg, configObj,
dhcpLink.getLink(), Property.V4_HEADER_FILENAME);
if ((filename != null) && !filename.isEmpty()) {
replyMsg.setFile(filename);
}
Map<Integer, DhcpOption> optionMap =
dhcpServerConfig.effectiveV4AddrOptions(requestMsg, dhcpLink, configObj);
if (DhcpServerPolicies.effectivePolicyAsBoolean(configObj,
dhcpLink.getLink(), Property.SEND_REQUESTED_OPTIONS_ONLY)) {
optionMap = requestedOptions(optionMap, requestMsg);
}
replyMsg.putAllDhcpOptions(optionMap);
// copy the relay agent info option from request to reply
// in order to echo option back to router as required
if (requestMsg.hasOption(DhcpConstants.V4OPTION_RELAY_INFO)) {
replyMsg.putDhcpOption(requestMsg.getDhcpOption(DhcpConstants.V4OPTION_RELAY_INFO));
}
}
/**
* Process the client request. Find appropriate configuration based on any
* criteria in the request message that can be matched against the server's
* configuration, then formulate a response message containing the options
* to be sent to the client.
*
* @return a Reply DhcpMessage
*/
public DhcpV4Message processMessage()
{
try {
if (!preProcess()) {
log.warn("Message dropped by preProcess");
return null;
}
if (log.isDebugEnabled()) {
log.debug("Processing: " + requestMsg.toStringWithOptions());
}
else if (log.isInfoEnabled()) {
log.info("Processing: " + requestMsg.toString());
}
// build a reply message using the local and remote sockets from the request
replyMsg = new DhcpV4Message(requestMsg.getLocalAddress(), requestMsg.getRemoteAddress());
replyMsg.setOp((short)DhcpConstants.V4_OP_REPLY);
// copy fields from request to reply
replyMsg.setHtype(requestMsg.getHtype());
replyMsg.setHlen(requestMsg.getHlen());
replyMsg.setTransactionId(requestMsg.getTransactionId());
replyMsg.setFlags(requestMsg.getFlags());
replyMsg.setGiAddr(requestMsg.getGiAddr());
replyMsg.setChAddr(requestMsg.getChAddr());
// MUST put Server Identifier in REPLY message
replyMsg.putDhcpOption(dhcpV4ServerIdOption);
if (!process()) {
log.warn("Message dropped by processor");
return null;
}
if (log.isDebugEnabled()) {
log.debug("Returning: " + replyMsg.toStringWithOptions());
}
else if (log.isInfoEnabled()) {
log.info("Returning: " + replyMsg.toString());
}
}
finally {
if (!postProcess()) {
log.warn("Message dropped by postProcess");
replyMsg = null;
}
}
return replyMsg;
}
/**
* Pre process.
*
* @return true if processing should continue
*/
public boolean preProcess()
{
InetSocketAddress localSocketAddr = requestMsg.getLocalAddress();
byte chAddr[] = requestMsg.getChAddr();
if ((chAddr == null) || (chAddr.length == 0) || isIgnoredMac(chAddr)) {
log.warn("Ignorning request message from client: mac=" +
Util.toHexString(chAddr));
return false;
}
clientLink = dhcpServerConfig.findDhcpLink(
(Inet4Address)localSocketAddr.getAddress(),
(Inet4Address)clientLinkAddress);
if (clientLink == null) {
log.error("No Link configured for DHCPv4 client request: " +
" localAddress=" + localSocketAddr.getAddress().getHostAddress() +
" clientLinkAddress=" + clientLinkAddress.getHostAddress());
return false; // must configure link for server to reply
}
/* TODO: check if this DOS mitigation is useful
*
boolean isNew = recentMsgs.add(requestMsg);
if (!isNew) {
if (log.isDebugEnabled())
log.debug("Dropping recent message");
return false; // don't process
}
if (log.isDebugEnabled())
log.debug("Processing new message");
long timer = DhcpServerPolicies.effectivePolicyAsLong(clientLink.getLink(),
Property.DHCP_PROCESSOR_RECENT_MESSAGE_TIMER);
if (timer > 0) {
recentMsgPruner.schedule(new RecentMsgTimerTask(requestMsg), timer);
}
*/
return true; // ok to process
}
/**
* Process.
*
* @return true if a reply should be sent
*/
public abstract boolean process();
/**
* Post process.
*
* @return true if a reply should be sent
*/
public boolean postProcess()
{
//TODO consider the implications of always removing the
// recently processed message b/c we could just keep
// getting blasted by an attempted DOS attack?
// Exactly!?... the comment above says it all
// if (recentMsgs.remove(requestMsg)) {
// if (log.isDebugEnabled())
// log.debug("Removed recent message: " + requestMsg.toString());
// }
return true;
}
/**
* Adds the v4 binding to reply.
*
* @param clientLink the client link
* @param binding the binding
*/
protected void addBindingToReply(DhcpLink clientLink, Binding binding)
{
Collection<BindingObject> bindingObjs = binding.getBindingObjects();
if ((bindingObjs != null) && !bindingObjs.isEmpty()) {
if (bindingObjs.size() == 1) {
BindingObject bindingObj = bindingObjs.iterator().next();
InetAddress inetAddr = bindingObj.getIpAddress();
if (inetAddr != null) {
replyMsg.setYiAddr(inetAddr);
// must be an DhcpV4OptionConfigObject for v4 binding
DhcpV4OptionConfigObject configObj =
(DhcpV4OptionConfigObject) bindingObj.getConfigObj();
if (configObj != null) {
long preferred = configObj.getPreferredLifetime();
DhcpV4LeaseTimeOption dhcpV4LeaseTimeOption = new DhcpV4LeaseTimeOption();
dhcpV4LeaseTimeOption.setUnsignedInt(preferred);
replyMsg.putDhcpOption(dhcpV4LeaseTimeOption);
populateV4Reply(clientLink, configObj);
//TODO when do actually start the timer? currently, two get
// created - one during advertise, one during reply
// policy to allow real-time expiration?
// bp.startExpireTimerTask(bindingAddr, iaAddrOption.getValidLifetime());
}
else {
log.error("Null binding pool in binding: " + binding.toString());
}
}
else {
log.error("Null address in binding: " + binding.toString());
}
}
else {
log.error("Expected only one bindingObject in v4 Binding, but found " +
bindingObjs.size() + "bindingObjects");
}
}
else {
log.error("No V4 bindings in binding object!");
}
}
/**
* Process ddns updates.
*/
protected void processDdnsUpdates(boolean sendUpdates)
{
boolean doForwardUpdate = true;
DhcpV4ClientFqdnOption clientFqdnOption =
(DhcpV4ClientFqdnOption) requestMsg.getDhcpOption(DhcpConstants.V4OPTION_CLIENT_FQDN);
DhcpV4HostnameOption hostnameOption =
(DhcpV4HostnameOption) requestMsg.getDhcpOption(DhcpConstants.V4OPTION_HOSTNAME);
if ((clientFqdnOption == null) && (hostnameOption == null)) {
//TODO allow name generation?
log.debug("No Client FQDN nor hostname option in request. Skipping DDNS update processing.");
return;
}
String fqdn = null;
String domain = DhcpServerPolicies.effectivePolicy(clientLink.getLink(), Property.DDNS_DOMAIN);
DhcpV4ClientFqdnOption replyFqdnOption = null;
if (clientFqdnOption != null) {
replyFqdnOption = new DhcpV4ClientFqdnOption();
replyFqdnOption.setDomainName(clientFqdnOption.getDomainName());
replyFqdnOption.setUpdateABit(false);
replyFqdnOption.setOverrideBit(false);
replyFqdnOption.setNoUpdateBit(false);
replyFqdnOption.setEncodingBit(clientFqdnOption.getEncodingBit());
replyFqdnOption.setRcode1((short)0xff); // RFC 4702 says server should set to 255
replyFqdnOption.setRcode2((short)0xff); // RFC 4702 says server should set to 255
fqdn = clientFqdnOption.getDomainName();
if ((fqdn == null) || (fqdn.length() <= 0)) {
log.error("Client FQDN option domain name is null/empty. No DDNS udpates performed.");
replyFqdnOption.setNoUpdateBit(true); // tell client that server did no updates
replyMsg.putDhcpOption(replyFqdnOption);
return;
}
String policy = DhcpServerPolicies.effectivePolicy(requestMsg,
clientLink.getLink(), Property.DDNS_UPDATE);
log.info("Server configuration for ddns.update policy: " + policy);
if ((policy == null) || policy.equalsIgnoreCase("none")) {
log.info("Server configuration for ddns.update policy is null or 'none'." +
" No DDNS updates performed.");
replyFqdnOption.setNoUpdateBit(true); // tell client that server did no updates
replyMsg.putDhcpOption(replyFqdnOption);
return;
}
if (clientFqdnOption.getNoUpdateBit() && policy.equalsIgnoreCase("honorNoUpdate")) {
log.info("Client FQDN NoUpdate flag set. Server configured to honor request." +
" No DDNS updates performed.");
replyFqdnOption.setNoUpdateBit(true); // tell client that server did no updates
replyMsg.putDhcpOption(replyFqdnOption);
//TODO: RFC 4704 Section 6.1
// ...the server SHOULD delete any RRs that it previously added
// via DNS updates for the client.
return;
}
if (!clientFqdnOption.getUpdateABit() && policy.equalsIgnoreCase("honorNoA")) {
log.info("Client FQDN NoA flag set. Server configured to honor request." +
" No FORWARD DDNS updates performed.");
doForwardUpdate = false;
}
else {
replyFqdnOption.setUpdateABit(true); // server will do update
if (!clientFqdnOption.getUpdateABit())
replyFqdnOption.setOverrideBit(true); // tell client that we overrode request flag
}
if ((domain != null) && !domain.isEmpty()) {
log.info("Server configuration for domain policy: " + domain);
// if there is a configured domain, then replace the domain provide by the client
int dot = fqdn.indexOf('.');
if (dot > 0) {
fqdn = fqdn.substring(0, dot+1) + domain;
}
else {
fqdn = fqdn + "." + domain;
}
replyFqdnOption.setDomainName(fqdn);
}
// since the client DID send option 81, return it in the reply
replyMsg.putDhcpOption(replyFqdnOption);
}
else {
// The client did not send an FQDN option, so we'll try to formulate the FQDN
// from the hostname option combined with the DDNS_DOMAIN policy setting.
// A replyFqdnOption is fabricated to be stored with the binding for use
// with the release/expire binding processing to remove the DDNS entry.
replyFqdnOption = new DhcpV4ClientFqdnOption();
fqdn = hostnameOption.getString();
if ((domain != null) && !domain.isEmpty()) {
log.info("Server configuration for domain policy: " + domain);
fqdn = fqdn + "." + domain;
// since the client did NOT send option 81, do not put
// the fabricated fqdnOption into the reply packet
// but set the option so that is can be used below
// when storing the fqdnOption to the database, so
// that it can be used if/when the lease expires
replyFqdnOption.setDomainName(fqdn);
// server will do the A record update, so set the flag
// for the option stored in the database, so server will
// remove the A record when the lease expires
replyFqdnOption.setUpdateABit(true);
}
else {
log.error("No DDNS domain configured. No DDNS udpates performed.");
replyFqdnOption.setNoUpdateBit(true); // tell client that server did no updates
replyMsg.putDhcpOption(replyFqdnOption);
return;
}
}
if (sendUpdates) {
for (Binding binding : bindings) {
if (binding.getState() == Binding.COMMITTED) {
Collection<BindingObject> bindingObjs = binding.getBindingObjects();
if (bindingObjs != null) {
for (BindingObject bindingObj : bindingObjs) {
V4BindingAddress bindingAddr = (V4BindingAddress) bindingObj;
DhcpConfigObject configObj = bindingAddr.getConfigObj();
DdnsCallback ddnsComplete =
new DhcpV4DdnsComplete(bindingAddr, replyFqdnOption);
DdnsUpdater ddns =
new DdnsUpdater(requestMsg, clientLink.getLink(), configObj,
bindingAddr.getIpAddress(), fqdn, requestMsg.getChAddr(),
configObj.getValidLifetime(), doForwardUpdate, false,
ddnsComplete);
ddns.processUpdates();
}
}
}
}
}
}
protected boolean addrOnLink(DhcpV4RequestedIpAddressOption requestedIpOption, DhcpLink clientLink)
{
boolean onLink = true;
if (requestedIpOption != null) {
try {
InetAddress requestedIp = InetAddress.getByName(requestedIpOption.getIpAddress());
if (!clientLink.getSubnet().contains(requestedIp)) {
onLink = false;
}
} catch (UnknownHostException ex) {
log.error("Invalid requested IP=" + requestedIpOption.getIpAddress() + ": " + ex);
}
}
return onLink;
}
protected boolean isIgnoredMac(byte[] chAddr)
{
String ignoredMacPolicy = DhcpServerPolicies.globalPolicy(Property.V4_IGNORED_MACS);
if (ignoredMacPolicy != null) {
String[] ignoredMacs = ignoredMacPolicy.split(",");
if (ignoredMacs != null) {
for (String ignoredMac : ignoredMacs) {
if (ignoredMac.trim().equalsIgnoreCase(Util.toHexString(chAddr))) {
return true;
}
}
}
}
return false;
}
/**
* The Class RecentMsgTimerTask.
*/
class RecentMsgTimerTask extends TimerTask
{
/** The dhcp msg. */
private DhcpV4Message dhcpMsg;
/**
* Instantiates a new recent msg timer task.
*
* @param dhcpMsg the dhcp msg
*/
public RecentMsgTimerTask(DhcpV4Message dhcpMsg)
{
this.dhcpMsg = dhcpMsg;
}
/* (non-Javadoc)
* @see java.util.TimerTask#run()
*/
@Override
public void run() {
if (recentMsgs.remove(dhcpMsg)) {
if (log.isDebugEnabled())
log.debug("Pruned recent message: " + dhcpMsg.toString());
}
}
}
}