# $Id: dhcp.bro 4054 2007-08-14 21:45:58Z pclin $ @load dpd @load weird module DHCP; export { # Set to false to disable printing to dhcp.log. const logging = T &redef; } # Type of states in DHCP client. See Figure 5 in RFC 2131. # Each state name is prefixed with DHCP_ to avoid name conflicts. type dhcp_state: enum { DHCP_INIT_REBOOT, DHCP_INIT, DHCP_SELECTING, DHCP_REQUESTING, DHCP_REBINDING, DHCP_BOUND, DHCP_RENEWING, DHCP_REBOOTING, # This state is not in Figure 5. Client has been externally configured. DHCP_INFORM, }; global dhcp_log: file; # Source port 68: client -> server; source port 67: server -> client. global dhcp_ports: set[port] = { 67/udp, 68/udp } &redef; redef dpd_config += { [ANALYZER_DHCP_BINPAC] = [$ports = dhcp_ports] }; # Default handling for peculiarities in DHCP analysis. redef Weird::weird_action += { ["DHCP_no_type_option"] = Weird::WEIRD_FILE, ["DHCP_wrong_op_type"] = Weird::WEIRD_FILE, ["DHCP_wrong_msg_type"] = Weird::WEIRD_FILE, }; # Types of DHCP messages, identified from the 'options' field. See RFC 1533. global dhcp_msgtype_name: table[count] of string = { [1] = "DHCP_DISCOVER", [2] = "DHCP_OFFER", [3] = "DHCP_REQUEST", [4] = "DHCP_DECLINE", [5] = "DHCP_ACK", [6] = "DHCP_NAK", [7] = "DHCP_RELEASE", [8] = "DHCP_INFORM", }; # Type of DHCP client state, inferred from the messages. See RFC 2131, fig 5. global dhcp_state_name: table[dhcp_state] of string = { [DHCP_INIT_REBOOT] = "INIT-REBOOT", [DHCP_INIT] = "INIT", [DHCP_SELECTING] = "SELECTING", [DHCP_REQUESTING] = "REQUESTING", [DHCP_REBINDING] = "REBINDING", [DHCP_BOUND] = "BOUND", [DHCP_RENEWING] = "RENEWING", [DHCP_REBOOTING] = "REBOOTING", [DHCP_INFORM] = "INFORM", }; type dhcp_session_info: record { state: dhcp_state; # the state of a DHCP client seq: count; # sequence of session in the trace lease: interval; # lease time of an IP address h_addr: string; # hardware/MAC address of the client }; # Track the DHCP session info of each client, indexed by the transaction ID. global dhcp_session: table[count] of dhcp_session_info &default = record($state = DHCP_INIT_REBOOT, $seq = 0, $lease = 0 sec, $h_addr = "") &write_expire = 5 min ; # We need the following table to track some DHCPINFORM messages since they # use xid = 0 (I do not know why), starting from the second pair of INFORM # and ACK. Since the client address is ready before DHCPINFORM, we can use # it as the index to find its corresponding xid. global session_xid: table[addr] of count &read_expire = 30 sec; # Count how many DHCP sessions have been detected, for use in dhcp_session_seq. global pkt_cnt: count = 0; global session_cnt: count = 0; # Record the address of client that sends a DHCPINFORM message with xid = 0. global recent_client: addr; global BROADCAST_ADDR = 255.255.255.255; global NULL_ADDR = 0.0.0.0; # Used to detect if an ACK is duplicated. They are used only in dhcp_ack(). # We put them here since Bro scripts lacks the equivalent of "static" variables. global ack_from: addr; global duplicated_ack: bool; function warning_wrong_state(msg_type: count): string { return fmt("%s not sent in a correct state.", dhcp_msgtype_name[msg_type]); } function dhcp_message(c: connection, seq: count, show_conn: bool): string { local conn_info = fmt("%.06f #%d", network_time(), seq); if ( show_conn ) return fmt("%s %s > %s", conn_info, endpoint_id(c$id$orig_h, c$id$orig_p), endpoint_id(c$id$resp_h, c$id$resp_p)); return conn_info; } function new_dhcp_session(xid: count, state: dhcp_state, h_addr: string) : dhcp_session_info { local session: dhcp_session_info; session$state = state; session$seq = ++session_cnt; session$lease = 0 sec; session$h_addr = h_addr; dhcp_session[xid] = session; return session; } event bro_init() { if ( logging ) dhcp_log = open_log_file("dhcp"); } event dhcp_discover(c: connection, msg: dhcp_msg, req_addr: addr) { local old_session = T; if ( msg$xid !in dhcp_session ) { local session = new_dhcp_session(msg$xid, DHCP_SELECTING, msg$h_addr); old_session = F; } if ( logging ) { if ( old_session && dhcp_session[msg$xid]$state == DHCP_SELECTING ) print dhcp_log, fmt("%s DISCOVER (duplicated)", dhcp_message(c, dhcp_session[msg$xid]$seq, F)); else print dhcp_log, fmt("%s DISCOVER (xid = %x, client state = %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, T), msg$xid, dhcp_state_name[dhcp_session[msg$xid]$state]); } } event dhcp_offer(c: connection, msg: dhcp_msg, mask: addr, router: dhcp_router_list, lease: interval, serv_addr: addr) { local standalone = msg$xid !in dhcp_session; local err_state = standalone && dhcp_session[msg$xid]$state != DHCP_SELECTING; if ( logging ) { # Note that no OFFER messages are considered duplicated, # since they may come from multiple DHCP servers in a session. if ( standalone ) print dhcp_log, fmt("%s OFFER (standalone)", dhcp_message(c, ++session_cnt, T)); else if ( err_state ) print dhcp_log, fmt("%s OFFER (in error state %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, T), dhcp_state_name[dhcp_session[msg$xid]$state]); else print dhcp_log, fmt("%s OFFER (client state = %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, T), dhcp_state_name[DHCP_SELECTING]); } } event dhcp_request(c: connection, msg: dhcp_msg, req_addr: addr, serv_addr: addr) { local log_info: string; if ( msg$xid in dhcp_session ) { if ( ! logging ) return; local state = dhcp_session[msg$xid]$state; if ( state == DHCP_REBOOTING ) recent_client = req_addr; else recent_client = c$id$orig_h; session_xid[recent_client] = msg$xid; if ( state == DHCP_RENEWING || state == DHCP_REBINDING || state == DHCP_REQUESTING || state == DHCP_REBOOTING ) print dhcp_log, fmt("%s REQUEST (duplicated)", dhcp_message(c, dhcp_session[msg$xid]$seq, F)); else { log_info = dhcp_message(c, dhcp_session[msg$xid]$seq, T); print dhcp_log, fmt("%s REQUEST (in error state %s)", log_info, dhcp_state_name[dhcp_session[msg$xid]$state]); } } else { local d_state = DHCP_REBOOTING; if ( c$id$resp_h != BROADCAST_ADDR ) d_state = DHCP_RENEWING; else if ( msg$ciaddr != NULL_ADDR ) d_state = DHCP_REBINDING; else if ( serv_addr != NULL_ADDR ) d_state = DHCP_REQUESTING; local session = new_dhcp_session(msg$xid, d_state, msg$h_addr); if ( session$state == DHCP_REBOOTING ) recent_client = req_addr; else recent_client = c$id$orig_h; session_xid[recent_client] = msg$xid; if ( logging ) { log_info = dhcp_message(c, session$seq, T); if ( req_addr != NULL_ADDR ) log_info = fmt("%s REQUEST %As", log_info, req_addr); else log_info = fmt("%s REQUEST", log_info); print dhcp_log, fmt("%s (xid = %x, client state = %s)", log_info, msg$xid, dhcp_state_name[session$state]); } } } event dhcp_decline(c: connection, msg: dhcp_msg) { local old_session = msg$xid in dhcp_session; local err_state = F; if ( old_session ) { if ( dhcp_session[msg$xid]$state == DHCP_REQUESTING ) dhcp_session[msg$xid]$state = DHCP_INIT; else err_state = T; } else new_dhcp_session(msg$xid, DHCP_INIT, ""); if ( ! logging ) return; if ( old_session ) { if ( err_state ) print dhcp_log, fmt("%s DECLINE (in error state %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, T), dhcp_state_name[dhcp_session[msg$xid]$state]); else print dhcp_log, fmt("%s DECLINE (duplicated)", dhcp_message(c, dhcp_session[msg$xid]$seq, F)); } else print dhcp_log, fmt("%s DECLINE (xid = %x)", dhcp_message(c, ++session_cnt, T), msg$xid); } event dhcp_ack(c: connection, msg: dhcp_msg, mask: addr, router: dhcp_router_list, lease: interval, serv_addr: addr) { local log_info: string; if ( msg$xid == 0 ) { # An ACK for a DHCPINFORM message with xid = 0. local xid = c$id$orig_h in session_xid ? # An ACK to the client. session_xid[c$id$orig_h] : # Assume ACK from a relay agent to the server. session_xid[recent_client]; local seq: count; if ( xid > 0 ) { duplicated_ack = dhcp_session[xid]$state != DHCP_INFORM; dhcp_session[xid]$state = DHCP_BOUND; seq = dhcp_session[xid]$seq; } else { # This is a weird situation. We arbitrarily set # duplicated_ack to false to have more information # shown. duplicated_ack = F; seq = session_cnt; } if ( ! logging ) return; log_info = dhcp_message(c, seq, F); if ( c$id$orig_h in session_xid ) { if ( duplicated_ack ) print dhcp_log, fmt("%s ACK (duplicated)", log_info); else print dhcp_log, fmt("%s ACK (client state = %s)", log_info, dhcp_state_name[DHCP_BOUND]); } else print dhcp_log, fmt("%s ACK (relay agent at = %As)", log_info, c$id$orig_h); return; } if ( msg$xid in dhcp_session ) { local last_state = dhcp_session[msg$xid]$state; local from_reboot_state = last_state == DHCP_REBOOTING; if ( last_state == DHCP_REQUESTING || last_state == DHCP_REBOOTING || last_state == DHCP_RENEWING || last_state == DHCP_REBINDING || last_state == DHCP_INFORM ) { dhcp_session[msg$xid]$state = DHCP_BOUND; dhcp_session[msg$xid]$lease = lease; } if ( ! logging ) return; if ( last_state == DHCP_BOUND ) { log_info = dhcp_message(c, dhcp_session[msg$xid]$seq, F); if ( c$id$orig_h == ack_from ) log_info = fmt("%s ACK (duplicated)", log_info); else # Not a duplicated ACK. log_info = fmt("%s ACK (relay agent at = %As)", log_info, c$id$orig_h); } else { ack_from = c$id$orig_h; # If in a reboot state, we had better # explicitly show the original address # and the destination address of ACK, # because the client initally has a # zero address. if ( from_reboot_state ) log_info = dhcp_message(c, dhcp_session[msg$xid]$seq, T); else log_info = dhcp_message(c, dhcp_session[msg$xid]$seq, F); if ( last_state != DHCP_INFORM && lease > 0 sec ) log_info = fmt("%s ACK (lease time = %s, ", log_info, lease); else log_info = fmt("%s ACK (", log_info); log_info = fmt("%sclient state = %s)", log_info, dhcp_state_name[dhcp_session[msg$xid]$state]); } print dhcp_log, log_info; } else if ( logging ) print dhcp_log, fmt("%s ACK (standalone)", dhcp_message(c, ++session_cnt, T)); } event dhcp_nak(c: connection, msg: dhcp_msg) { if ( msg$xid in dhcp_session ) { local last_state = dhcp_session[msg$xid]$state; if ( last_state == DHCP_REQUESTING || last_state == DHCP_REBOOTING || last_state == DHCP_RENEWING || last_state == DHCP_REBINDING ) dhcp_session[msg$xid]$state = DHCP_INIT; if ( logging ) print dhcp_log, fmt("%s NAK (client state = %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, F), dhcp_state_name[dhcp_session[msg$xid]$state]); } else if ( logging ) print dhcp_log, fmt("%s NAK (standalone)", dhcp_message(c, ++session_cnt, T)); } event dhcp_release(c: connection, msg: dhcp_msg) { local old_session = msg$xid in dhcp_session; if ( ! old_session ) # We assume the client goes back to DHCP_INIT # because the RFC does not specify which state to go to. new_dhcp_session(msg$xid, DHCP_INIT, ""); if ( ! logging ) return; if ( old_session ) { if ( dhcp_session[msg$xid]$state == DHCP_INIT ) print dhcp_log, fmt("%s RELEASE (duplicated)", dhcp_message(c, dhcp_session[msg$xid]$seq, F)); else print dhcp_log, fmt("%s RELEASE, (client state = %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, F), dhcp_state_name[dhcp_session[msg$xid]$state]); } else print dhcp_log, fmt("%s RELEASE (xid = %x, IP addr = %As)", dhcp_message(c, session_cnt, T), msg$xid, c$id$orig_h); } event dhcp_inform(c: connection, msg: dhcp_msg) { recent_client = c$id$orig_h; if ( msg$xid == 0 ) { # Oops! Try to associate message with transaction ID 0 with # a previous session. local xid: count; local seq: count; if ( c$id$orig_h in session_xid ) { xid = session_xid[c$id$orig_h]; dhcp_session[xid]$state = DHCP_INFORM; seq = dhcp_session[xid]$seq; } else { # Weird: xid = 0 and no previous INFORM-ACK dialog. xid = 0; seq = ++session_cnt; # Just record that a INFORM message has appeared, # although the xid is not useful. session_xid[c$id$orig_h] = 0; } if ( logging ) print dhcp_log, fmt("%s INFORM (xid = %x, client state = %s)", dhcp_message(c, seq, T), xid, dhcp_state_name[DHCP_INFORM]); return; } if ( msg$xid in dhcp_session ) { if ( logging ) if ( dhcp_session[msg$xid]$state == DHCP_INFORM ) print dhcp_log, fmt("%s INFORM (duplicated)", dhcp_message(c, dhcp_session[msg$xid]$seq, F)); else { print dhcp_log, fmt("%s INFORM (duplicated, client state = %s)", dhcp_message(c, dhcp_session[msg$xid]$seq, F), dhcp_state_name[dhcp_session[msg$xid]$state]); } return; } local session = new_dhcp_session(msg$xid, DHCP_INFORM, msg$h_addr); # Associate this transaction ID with the host so we can identify # subsequent pairs of INFORM/ACK if client uses xid=0. session_xid[c$id$orig_h] = msg$xid; if ( logging ) print dhcp_log, fmt("%s INFORM (xid = %x, client state = %s)", dhcp_message(c, session$seq, T), msg$xid, dhcp_state_name[session$state]); }