/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2006-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) 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.
*
* OpenNMS(R) 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 OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.dhcpd;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.StringTokenizer;
import org.opennms.core.utils.InetAddressUtils;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.config.dhcpd.DhcpdConfigFactory;
import org.opennms.netmgt.utils.IpValidator;
import edu.bucknell.net.JDHCP.DHCPMessage;
/**
* <P>
* Establishes a TCP socket connection with the DHCP daemon and formats and
* sends request messages.
* </P>
*
* @author <A HREF="mailto:mike@opennms.org">Mike </A>
* @author <A HREF="http://www.opennms.org/">OpenNMS </A>
* @version CVS 1.1.1.1
*/
final class Poller {
/**
* The hardware address (ex: 00:06:0D:BE:9C:B2)
*/
private static final byte[] DEFAULT_MAC_ADDRESS = { (byte) 0x00,
(byte) 0x06, (byte) 0x0d, (byte) 0xbe, (byte) 0x9c, (byte) 0xb2,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
private static byte[] s_hwAddress = null;
private static byte[] s_myIpAddress = null;
private static byte[] s_requestIpAddress = null;
private static boolean reqTargetIp = true;
private static boolean targetOffset = true;
private static boolean relayMode = false;
private static boolean paramsChecked = false;
private static Boolean extendedMode = false;
/**
* Broadcast flag...when set in the 'flags' portion of the DHCP query
* packet, it forces the DHCP server to broadcast the DHCP response. This
* is useful when we are not setting the relay address in the outgoing
* DHCP query. Otherwise, we would not receive the response.
*/
static final short BROADCAST_FLAG = (short) 0x8000;
/**
* Default retries
*/
static final int DEFAULT_RETRIES = 2;
/**
* Default timeout
*/
static final long DEFAULT_TIMEOUT = 3000L;
/**
* The message type option for the DHCP request.
*/
private static final int MESSAGE_TYPE = 53;
/**
* The requested ip option for the DHCP request.
*/
private static final int REQUESTED_IP = 50;
/**
* Holds the value for the next identifier sent to the DHCP server.
*/
private static int m_nextXid = (new java.util.Random(System.currentTimeMillis())).nextInt();
/**
* TCP Socket connection with DHCP Daemon
*/
private Socket m_connection;
/**
* Output Object stream
*/
private ObjectOutputStream m_outs;
/**
* Objects from the server.
*/
private ObjectInputStream m_ins;
/**
* Returns a disconnection request message that can be sent to the server.
*
* @return A disconnection message.
*/
private static Message getDisconnectRequest() throws UnknownHostException {
return new Message(InetAddressUtils.addr("0.0.0.0"), new DHCPMessage());
}
/**
* Returns a DHCP DISCOVER, INFORM, or REQUEST message that can be sent to
* the DHCP server. DHCP server should respond with a DHCP OFFER, ACK, or
* NAK message in response..
*
* @param (InetAddress) addr The address to poll
* @param (byte) mType The type of DHCP message to send (DISCOVER, INFORM,
* or REQUEST)
* @return The message to send to the DHCP server.
*/
private static Message getPollingRequest(InetAddress addr, byte mType) {
int xid = 0;
synchronized (Poller.class) {
xid = ++m_nextXid;
}
DHCPMessage messageOut = new DHCPMessage();
byte[] rawIp = addr.getAddress();
// if targetOffset = true, we don't want to REQUEST the DHCP server's
// own IP, so change it by 1, trying to avoid the subnet address
// and the broadcast address.
if (targetOffset) {
if (rawIp[3] % 2 == 0 && rawIp[3] != 0) {
--rawIp[3];
} else {
++rawIp[3];
}
}
// fill DHCPMessage object
//
messageOut.setOp((byte) 1);
messageOut.setHtype((byte) 1);
messageOut.setHlen((byte) 6);
messageOut.setXid(xid);
messageOut.setSecs((short) 0);
messageOut.setChaddr(s_hwAddress); // set hardware address
if (relayMode) {
messageOut.setHops((byte) 1);
messageOut.setGiaddr(s_myIpAddress); // set relay address for replies
} else {
messageOut.setHops((byte) 0);
messageOut.setFlags(BROADCAST_FLAG);
}
messageOut.setOption(MESSAGE_TYPE, new byte[] { mType });
if (mType == DHCPMessage.REQUEST) {
if (reqTargetIp) {
messageOut.setOption(REQUESTED_IP, rawIp);
messageOut.setCiaddr(rawIp);
} else {
messageOut.setOption(REQUESTED_IP, s_requestIpAddress);
messageOut.setCiaddr(s_requestIpAddress);
}
}
if (mType == DHCPMessage.INFORM) {
messageOut.setOption(REQUESTED_IP, s_myIpAddress);
messageOut.setCiaddr(s_myIpAddress);
}
return new Message(addr, messageOut);
}
/**
* Ensures that during garbage collection the resources used by this
* object are released!
*
* @throws java.lang.Throwable if any.
*/
protected void finalize() throws Throwable {
close();
}
/**
* Constructor. Establishes a TCP socket connection with the DHCP client
* daemon on port 5818.
*
* @throws IOException
* if unable to establish the connection with the DHCP client
* daemon.
*/
private Poller(long timeout) throws IOException {
ThreadCategory log = ThreadCategory.getInstance(this.getClass());
DhcpdConfigFactory dcf = DhcpdConfigFactory.getInstance();
try {
if (log.isDebugEnabled()) {
log.debug("Poller.ctor: opening socket connection with DHCP client daemon on port " + dcf.getPort());
}
m_connection = new Socket(InetAddressUtils.addr("127.0.0.1"), dcf.getPort());
if (log.isDebugEnabled()) {
log.debug("Poller.ctor: setting socket timeout to " + timeout);
}
m_connection.setSoTimeout((int) timeout);
// Establish input/output object streams
m_ins = new ObjectInputStream(m_connection.getInputStream());
m_outs = new ObjectOutputStream(m_connection.getOutputStream());
m_outs.reset();
m_outs.flush();
} catch (IOException ex) {
log.error("IO Exception during socket connection establishment with DHCP client daemon.", ex);
if (m_connection != null) {
try {
m_ins.close();
m_outs.close();
m_connection.close();
} catch (Throwable t) {
}
}
throw ex;
} catch (Throwable t) {
log.error("Unexpected exception during socket connection establishment with DHCP client daemon.", t);
if (m_connection != null) {
try {
m_ins.close();
m_outs.close();
m_connection.close();
} catch (Throwable tx) {
}
}
throw new UndeclaredThrowableException(t);
}
}
/**
* Closes the client's socket connection to the DHCP daemon.
*
* @throws IOException
* if the socket close() method fails.
*/
public void close() {
ThreadCategory log = ThreadCategory.getInstance(Poller.class);
try {
if (log.isDebugEnabled()) {
log.debug("Closing connection");
}
m_ins.close();
m_outs.close();
m_connection.close();
} catch (Throwable ex) {
}
}
/**
* <p>
* This method actually tests the remote host to determine if it is
* running a functional DHCP server.
* </p>
* <p>
* Formats a DHCP query and encodes it in a client request message which
* is sent to the DHCP daemon over the established TCP socket connection.
* If a matching DHCP response packet is not received from the DHCP daemon
* within the specified timeout the client request message will be re-sent
* up to the specified number of retries.
* </p>
* <p>
* If a response is received from the DHCP daemon it is validated to
* ensure that:
* </p>
* <ul>
* <li>The DHCP response packet was sent from the remote host to which the
* original request packet was directed.</li>
* <li>The XID of the DHCP response packet matches the XID of the original
* DHCP request packet.</li>
* </ul>
* <p>
* If the response validates 'true' is returned. Otherwise the request is
* resent until max retry count is exceeded.
* </p>
* <p>
* Before returning, a client disconnect message (remote host field set to
* zero) is sent to the DHCP daemon.
* </p>
*
* @return response time in milliseconds if the specified host responded
* with a valid DHCP offer datagram within the context of the
* specified timeout and retry values or negative one (-1)
* otherwise.
*/
static long isServer(InetAddress host, long timeout, int retries) throws IOException {
ThreadCategory log = ThreadCategory.getInstance(Poller.class);
boolean isDhcpServer = false;
// List of DHCP queries to try. The default when extended
// mode = false must be listed first. (DISCOVER)
byte[] typeList = { (byte) DHCPMessage.DISCOVER, (byte) DHCPMessage.INFORM, (byte) DHCPMessage.REQUEST };
String[] typeName = { "DISCOVER", "INFORM", "REQUEST" };
DhcpdConfigFactory dcf = DhcpdConfigFactory.getInstance();
if (!paramsChecked) {
String s_extendedMode = dcf.getExtendedMode();
if (s_extendedMode == null) {
extendedMode = false;
} else {
extendedMode = Boolean.parseBoolean(s_extendedMode);
}
if (log.isDebugEnabled()) {
log.debug("isServer: DHCP extended mode is " + extendedMode);
}
String hwAddressStr = dcf.getMacAddress();
if (log.isDebugEnabled()) {
log.debug("isServer: DHCP query hardware/MAC address is " + hwAddressStr);
}
setHwAddress(hwAddressStr);
String myIpStr = dcf.getMyIpAddress();
if (log.isDebugEnabled()) {
log.debug("isServer: DHCP relay agent address is " + myIpStr);
}
if (myIpStr == null || myIpStr.equals("") || myIpStr.equalsIgnoreCase("broadcast")) {
// do nothing
} else if (IpValidator.isIpValid(myIpStr)) {
s_myIpAddress = setIpAddress(myIpStr);
relayMode = true;
}
if (extendedMode == true) {
String requestStr = dcf.getRequestIpAddress();
if (log.isDebugEnabled()) {
log.debug("isServer: REQUEST query target is " + requestStr);
}
if (requestStr == null || requestStr.equals("") || requestStr.equalsIgnoreCase("targetSubnet")) {
// do nothing
} else if (requestStr.equalsIgnoreCase("targetHost")) {
targetOffset = false;
} else if (IpValidator.isIpValid(requestStr)) {
s_requestIpAddress = setIpAddress(requestStr);
reqTargetIp = false;
targetOffset = false;
}
if (log.isDebugEnabled()) {
log.debug("REQUEST query options are: reqTargetIp = " + reqTargetIp + ", targetOffset = " + targetOffset);
}
}
paramsChecked = true;
}
int j = 1;
if (extendedMode == true) {
j = typeList.length;
}
if (timeout < 500) {
timeout = 500;
}
Poller p = new Poller(timeout);
long responseTime = -1;
try {
pollit: for (int i = 0; i < j; i++) {
Message ping = getPollingRequest(host, (byte) typeList[i]);
int rt = retries;
while (rt >= 0 && !isDhcpServer) {
if (log.isDebugEnabled()) {
log.debug("isServer: sending DHCP " + typeName[i] + " query to host " + InetAddressUtils.str(host) + " with Xid: " + ping.getMessage().getXid());
}
long start = System.currentTimeMillis();
p.m_outs.writeObject(ping);
long end;
do {
Message resp = null;
try {
resp = (Message) p.m_ins.readObject();
} catch (InterruptedIOException ex) {
resp = null;
}
if (resp != null) {
responseTime = System.currentTimeMillis() - start;
// DEBUG only
if (log.isDebugEnabled()) {
log.debug("isServer: got a DHCP response from host " + InetAddressUtils.str(resp.getAddress()) + " with Xid: " + resp.getMessage().getXid());
}
if (host.equals(resp.getAddress()) && ping.getMessage().getXid() == resp.getMessage().getXid()) {
// Inspect response message to see if it is a valid DHCP response
byte[] type = resp.getMessage().getOption(MESSAGE_TYPE);
if (log.isDebugEnabled()) {
if (type[0] == DHCPMessage.OFFER) {
log.debug("isServer: got a DHCP OFFER response, validating...");
} else if (type[0] == DHCPMessage.ACK) {
log.debug("isServer: got a DHCP ACK response, validating...");
} else if (type[0] == DHCPMessage.NAK) {
log.debug("isServer: got a DHCP NAK response, validating...");
}
}
// accept offer or ACK or NAK
if (type[0] == DHCPMessage.OFFER || (extendedMode == true && (type[0] == DHCPMessage.ACK || type[0] == DHCPMessage.NAK))) {
if (log.isDebugEnabled()) {
log.debug("isServer: got a valid DHCP response. responseTime= " + responseTime + "ms");
}
isDhcpServer = true;
break pollit;
}
}
}
end = System.currentTimeMillis();
} while ((end - start) < timeout);
if (log.isDebugEnabled()) {
if (!isDhcpServer) {
log.debug("Timed out waiting for DHCP response, remaining retries: " + rt);
}
}
--rt;
}
}
if (log.isDebugEnabled()) {
log.debug("Sending disconnect request");
}
p.m_outs.writeObject(getDisconnectRequest());
if (log.isDebugEnabled()) {
log.debug("wait half a sec before closing connection");
}
Thread.sleep(500);
p.close();
} catch (IOException ex) {
log.error("IO Exception caught.", ex);
p.close();
throw ex;
} catch (Throwable t) {
log.error("Unexpected Exception caught.", t);
p.close();
throw new UndeclaredThrowableException(t);
}
// Return response time if the remote box IS a DHCP
// server or -1 if the remote box is NOT a DHCP server.
if (isDhcpServer) {
return responseTime;
} else {
return -1;
}
}
// Converts the provided hardware address string (format=00:00:00:00:00:00)
// to an array of bytes which can be passed in a DHCP DISCOVER packet.
private static void setHwAddress(String hwAddressStr) {
ThreadCategory log = ThreadCategory.getInstance(Poller.class);
// initialize the address
s_hwAddress = DEFAULT_MAC_ADDRESS;
StringTokenizer token = new StringTokenizer(hwAddressStr, ":");
if (token.countTokens() != 6) {
if (log.isDebugEnabled()) {
log.debug("Invalid format for hwAddress " + hwAddressStr);
}
}
int temp;
int i = 0;
while (i < 6) {
try {
temp = Integer.parseInt(token.nextToken(), 16);
s_hwAddress[i] = (byte) temp;
i++;
} catch (NumberFormatException ex) {
if (log.isDebugEnabled()) {
log.debug("Invalid format for hwAddress, " + ex);
}
}
}
}
// Converts the provided IP address string
// to an array of bytes which can be passed in a DHCP packet.
private static byte[] setIpAddress(String ipAddressStr) {
// initialize the address
byte[] ipAddress = new byte[4];
StringTokenizer token = new StringTokenizer(ipAddressStr, ".");
int temp;
int i = 0;
while (i < 4) {
temp = Integer.parseInt(token.nextToken(), 10);
ipAddress[i] = (byte) temp;
i++;
}
return ipAddress;
}
}