diff --git a/scripts/base/protocols/dhcpv6/__load__.zeek b/scripts/base/protocols/dhcpv6/__load__.zeek index c04423a855..fc9a034c35 100644 --- a/scripts/base/protocols/dhcpv6/__load__.zeek +++ b/scripts/base/protocols/dhcpv6/__load__.zeek @@ -1,4 +1,7 @@ +@if ( have_spicy_analyzers() ) # prototypes below must not be used with legacy analyzer @load ./consts +@load ./spicy-events @load ./main @load-sigs ./dpd.sig +@endif diff --git a/scripts/base/protocols/dhcpv6/consts.zeek b/scripts/base/protocols/dhcpv6/consts.zeek index cc639271fd..49cfdfa8ce 100644 --- a/scripts/base/protocols/dhcpv6/consts.zeek +++ b/scripts/base/protocols/dhcpv6/consts.zeek @@ -2,11 +2,206 @@ module DHCPv6; export { const message_types = { - [1] = "SOLICIT", - } &default = function(n: count): string { return fmt("unknown-message-type-%d", n); }; + [1] = "SOLICIT", + [2] = "ADVERTISE", + [3] = "REQUEST", + [4] = "CONFIRM", + [5] = "RENEW", + [6] = "REBIND", + [7] = "REPLY", + [8] = "RELEASE", + [9] = "DECLINE", + [10] = "RECONFIGURE", + [11] = "INFORMATION_REQUEST", + } &default = function(n: count): string { return fmt("unk-%d", n); }; + + ## DUID types + const duid_types = { + [1] = "LLT", + [2] = "EN", + [3] = "LL", + [4] = "UUID", + } &default = function(n: count): string { return fmt("unk-%d", n); }; + + ## Status codes + # Full list https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#dhcpv6-parameters-5 + const status_codes = { + [0] = "Success", + [1] = "UnspecFail", + [2] = "NoAddrsAvail", + [3] = "NoBinding", + [4] = "NotOnLink", + [5] = "UseMulticast", + [6] = "NoPrefixAvail", + [7] = "UnknownQueryType", + [8] = "MalformedQuery", + [9] = "NotConfigured", + [10] = "NotAllowed", + [11] = "QueryTerminated", + [12] = "DataMissing", + [13] = "CatchUpComplete", + [14] = "NotSupported", + [15] = "TLSConnectionRefused", + [16] = "AddressInUse", + [17] = "ConfigurationConflict", + [18] = "MissingBindingInformation", + [19] = "OutdatedBindingInformation", + [20] = "ServerShuttingDown", + [21] = "DNSUpdateNotSupported", + [22] = "ExcessiveTimeSkew", + } &default = function(n: count): string { return fmt("unk-%d", n); }; + ## Option types mapped to their names. const option_types = { - [1] = "???", - } &default = function(n: count): string { return fmt("unknown-option-type-%d", n); }; + [0] = "Reserved", + [1] = "CLIENTID", + [2] = "SERVERID", + [3] = "IA_NA", + [4] = "IA_TA", + [5] = "IAADDR", + [6] = "ORO", + [7] = "PREFERENCE", + [8] = "ELAPSED_TIME", + [9] = "RELAY_MSG", + [10] = "Unassigned", + [11] = "AUTH", + [12] = "UNICAST", + [13] = "STATUS_CODE", + [14] = "RAPID_COMMIT", + [15] = "USER_CLASS", + [16] = "VENDOR_CLASS", + [17] = "VENDOR_OPTS", + [18] = "INTERFACE_ID", + [19] = "RECONF_MSG", + [20] = "RECONF_ACCEPT", + [21] = "SIP_SERVER_D", + [22] = "SIP_SERVER_A", + [23] = "DNS_SERVERS", + [24] = "DOMAIN_LIST", + [25] = "IA_PD", + [26] = "IAPREFIX", + [27] = "NIS_SERVERS", + [28] = "NISP_SERVERS", + [29] = "NIS_DOMAIN_NAME", + [30] = "NISP_DOMAIN_NAME", + [31] = "SNTP_SERVERS", + [32] = "INFORMATION_REFRESH_TIME", + [33] = "BCMCS_SERVER_D", + [34] = "BCMCS_SERVER_A", + [35] = "Unassigned", + [36] = "GEOCONF_CIVIC", + [37] = "REMOTE_ID", + [38] = "SUBSCRIBER_ID", + [39] = "CLIENT_FQDN", + [40] = "PANA_AGENT", + [41] = "NEW_POSIX_TIMEZONE", + [42] = "NEW_TZDB_TIMEZONE", + [43] = "ERO", + [44] = "LQ_QUERY", + [45] = "CLIENT_DATA", + [46] = "CLT_TIME", + [47] = "LQ_RELAY_DATA", + [48] = "LQ_CLIENT_LINK", + [49] = "MIP6_HNIDF", + [50] = "MIP6_VDINF", + [51] = "V6_LOST", + [52] = "CAPWAP_AC_V6", + [53] = "RELAY_ID", + [54] = "IPv6_Address-MoS", + [55] = "IPv6_FQDN-MoS", + [56] = "NTP_SERVER", + [57] = "V6_ACCESS_DOMAIN", + [58] = "SIP_UA_CS_LIST", + [59] = "OPT_BOOTFILE_URL", + [60] = "OPT_BOOTFILE_PARAM", + [61] = "CLIENT_ARCH_TYPE", + [62] = "NII", + [63] = "GEOLOCATION", + [64] = "AFTR_NAME", + [65] = "ERP_LOCAL_DOMAIN_NAME", + [66] = "RSOO", + [67] = "PD_EXCLUDE", + [68] = "VSS", + [69] = "MIP6_IDINF", + [70] = "MIP6_UDINF", + [71] = "MIP6_HNP", + [72] = "MIP6_HAA", + [73] = "MIP6_HAF", + [74] = "RDNSS_SELECTION", + [75] = "KRB_PRINCIPAL_NAME", + [76] = "KRB_REALM_NAME", + [77] = "KRB_DEFAULT_REALM_NAME", + [78] = "KRB_KDC", + [79] = "CLIENT_LINKLAYER_ADDR", + [80] = "LINK_ADDRESS", + [81] = "RADIUS", + [82] = "SOL_MAX_RT", + [83] = "INF_MAX_RT", + [84] = "ADDRSEL", + [85] = "ADDRSEL_TABLE", + [86] = "V6_PCP_SERVER", + [87] = "DHCPV4_MSG", + [88] = "DHCP4_O_DHCP6_SERVER", + [89] = "S46_RULE", + [90] = "S46_BR", + [91] = "S46_DMR", + [92] = "S46_V4V6BIND", + [93] = "S46_PORTPARAMS", + [94] = "S46_CONT_MAPE", + [95] = "S46_CONT_MAPT", + [96] = "S46_CONT_LW", + [97] = "4RD", + [98] = "4RD_MAP_RULE", + [99] = "4RD_NON_MAP_RULE", + [100] = "LQ_BASE_TIME", + [101] = "LQ_START_TIME", + [102] = "LQ_END_TIME", + [103] = "DHCP_Captive_Portal", + [104] = "MPL_PARAMETERS", + [105] = "ANI_ATT", + [106] = "ANI_NETWORK_NAME", + [107] = "ANI_AP_NAME", + [108] = "ANI_AP_BSSID", + [109] = "ANI_OPERATOR_ID", + [110] = "ANI_OPERATOR_REALM", + [111] = "S46_PRIORITY", + [112] = "MUD_URL_V6", + [113] = "V6_PREFIX64", + [114] = "F_BINDING_STATUS", + [115] = "F_CONNECT_FLAGS", + [116] = "F_DNS_REMOVAL_INFO", + [117] = "F_DNS_HOST_NAME", + [118] = "F_DNS_ZONE_NAME", + [119] = "F_DNS_FLAGS", + [120] = "F_EXPIRATION_TIME", + [121] = "F_MAX_UNACKED_BNDUPD", + [122] = "F_MCLT", + [123] = "F_PARTNER_LIFETIME", + [124] = "F_PARTNER_LIFETIME_SENT", + [125] = "F_PARTNER_DOWN_TIME", + [126] = "F_PARTNER_RAW_CLT_TIME", + [127] = "F_PROTOCOL_VERSION", + [128] = "F_KEEPALIVE_TIME", + [129] = "F_RECONFIGURE_DATA", + [130] = "F_RELATIONSHIP_NAME", + [131] = "F_SERVER_FLAGS", + [132] = "F_SERVER_STATE", + [133] = "F_START_TIME_OF_STATE", + [134] = "F_STATE_EXPIRATION_TIME", + [135] = "RELAY_PORT", + [136] = "V6_SZTP_REDIRECT", + [137] = "S46_BIND_IPV6_PREFIX", + [138] = "IA_LL", + [139] = "LLADDR", + [140] = "SLAP_QUAD", + [141] = "V6_DOTS_RI", + [142] = "V6_DOTS_ADDRESS", + [143] = "IPv6_Address-ANDSF", + [144] = "V6_DNR", + [145] = "REGISTERED_DOMAIN", + [146] = "FORWARD_DIST_MANAGER", + [147] = "REVERSE_DIST_MANAGER", + [148] = "ADDR_REG_ENABLE", + } &default = function(n: count): string { return fmt("unk-%d", n); }; } diff --git a/scripts/base/protocols/dhcpv6/main.zeek b/scripts/base/protocols/dhcpv6/main.zeek index 83d3281817..69202a50a4 100644 --- a/scripts/base/protocols/dhcpv6/main.zeek +++ b/scripts/base/protocols/dhcpv6/main.zeek @@ -8,21 +8,79 @@ export { global log_policy: Log::PolicyHook; + type DUID: record { + ## Type of DUID in string format. + typ: string; + ## DUID in hex format. + data: string; + } &log; + + type Status: record { + code: string; + message: string; + } &log; + + type IA_NA: record { + iaid: count &log &optional; + t1: interval &log &optional; + t2: interval &log &optional; + iaaddr: addr &optional; # This could be more than just one. + } &log; + ## The record type which contains the column fields of the DHCP log. type Info: record { ## The earliest time at which a DHCP message over the ## associated connection is observed. - ts: time &log; + ts: time &log; + + ## Transaction ID. + transaction_id: count &log; + + client_msg_type: string &log &optional; + server_msg_type: string &log &optional; + + client_duid: DUID &log &optional; + server_duid: DUID &log &optional; + + client_options: vector of string &log; + client_requested_options: vector of string &log &optional; + server_options: vector of string &log; + + + ## Involved connection uids for this transaction. + uids: set[string] &log; + + ## Information of the *first* ia_na option given + ## by the server. + ia_na: IA_NA &log &optional; + + status: Status &log &optional; + + ## If the server provided a jj + client_fqdn: string &log &optional; + + logged: bool &default=F; + }; + + type State: record { + info: Info; + cid: conn_id; + uid: string; + is_client: bool; }; ## Event that can be handled to access the DHCP ## record as it is sent on to the logging framework. global log_dhcpv6: event(rec: Info); + + option transaction_timeout = 5sec; + + global aggregate_msgs: event(c: State); } # Add the dhcp info to the connection record. redef record connection += { - dhcpv6: Info &optional; + dhcpv6_state: State &optional; }; const ports = { 546/udp, 547/udp }; @@ -30,15 +88,110 @@ redef likely_server_ports += { 547/udp }; event zeek_init() &priority=5 { - Log::create_stream(DHCP::LOG, [$columns=Info, $ev=log_dhcpv6, $path="dhcpv6", $policy=log_policy]); + Log::create_stream(LOG, [$columns=Info, $ev=log_dhcpv6, $path="dhcpv6", $policy=log_policy]); Analyzer::register_for_ports(Analyzer::ANALYZER_DHCPV6, ports); } +function do_log(tbl: table[count] of State, transaction_id: count): interval + { + local rec = tbl[transaction_id]$info; + print "expire_func", rec; + if ( ! rec$logged ) + { + Log::write(LOG, rec); + rec$logged = T; + } + + return 0sec; + } + +## On manager, globally? +global transactions: table[count] of State &write_expire=transaction_timeout &expire_func=do_log; + +function merge(into_state: State, from_state: State): Info + { + local into = into_state$info; + local from = from_state$info; + local from_is_client = from_state$is_client; + local from_is_server = ! from_is_client; + + add into$uids[from_state$uid]; + + if ( from_is_server ) + { + if ( from?$status ) + into$status = from$status; + + if ( from?$server_msg_type ) + into$server_msg_type = from$server_msg_type; + + into$server_options = from$server_options; + + if ( from?$server_duid ) + into$server_duid = from$server_duid; + + if ( from?$ia_na ) + into$ia_na = from$ia_na; + } + + return into; + } + +event aggregate_msgs(state: State) + { + local txid = state$info$transaction_id; + print "aggregate", state$is_client; + + # First time we see this transaction, just store it. + if ( txid !in transactions ) + { + transactions[txid] = state; + return; + } + + local into_state = transactions[txid]; + if ( into_state$is_client == state$is_client ) + { + # Repeated send from client or server. Is this weird? + Weird::weird([$ts=network_time(), $uid=state$uid, $name="dhcpv6_resend"]); + Log::write(LOG, into_state$info); + transactions[txid] = state; + } + else + { + if ( ! into_state$is_client ) + Weird::weird([$ts=network_time(), $uid=into_state$uid, $name="dhcpv6_server_before_client"]); + + local info = merge(into_state, state); + Log::write(LOG, info); + + # We do not delete the record immediately so that for a + # single client request that doesn't use SERVERID, we + # might process further replies. + info$logged = T; + } + } + +function set_state(c: connection, is_orig: bool, transaction_id: count): State + { + print "set_state", c$id, is_orig; + c$dhcpv6_state = State($cid=c$id, $uid=c$uid, $is_client=is_orig); + c$dhcpv6_state$info = Info($ts=network_time(), $transaction_id=transaction_id); + add c$dhcpv6_state$info$uids[c$uid]; + + return c$dhcpv6_state; + } # Aggregate DHCP messages to the manager. -event dhcpv6_message(c: connection, is_orig: bool) +event dhcpv6_message(c: connection, is_orig: bool, msg_type: count, transaction_id: count) { - print c$uid, c$id, is_orig; + print "XXX dhcpv6_message", c$uid, c$id, is_orig, message_types[msg_type]; + local state = set_state(c, is_orig, transaction_id); + if ( state$is_client ) + state$info$client_msg_type = message_types[msg_type]; + else + state$info$server_msg_type = message_types[msg_type]; + # print "dhcpv6_message", c$uid, c$id, is_orig; # if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER ) # Broker::publish(Cluster::manager_topic, DHCP::aggregate_msgs, # network_time(), c$id, c$uid, is_orig, msg, options); @@ -46,9 +199,95 @@ event dhcpv6_message(c: connection, is_orig: bool) # event DHCP::aggregate_msgs(network_time(), c$id, c$uid, is_orig, msg, options); } +event dhcpv6_option(c: connection, is_orig: bool, code: count) + { + print "option", option_types[code]; + local info = c$dhcpv6_state$info; + local opts = c$dhcpv6_state$is_client ? info$client_options : info$server_options; + opts += option_types[code]; + } + +event dhcpv6_option_clientid(c: connection, is_orig: bool, duid_type: count, data: string) + { + local info = c$dhcpv6_state$info; + info$client_duid = [$typ=duid_types[duid_type], $data=bytestring_to_hexstr(data)]; + } + +event dhcpv6_option_serverid(c: connection, is_orig: bool, duid_type: count, data: string) + { + local info = c$dhcpv6_state$info; + info$server_duid = [$typ=duid_types[duid_type], $data=bytestring_to_hexstr(data)]; + } + +event dhcpv6_option_status_code(c: connection, is_orig: bool, code: count, message: string) + { + local info = c$dhcpv6_state$info; + info$status = [$code=status_codes[code], $message=message]; + } + +event dhcpv6_option_requested_options(c: connection, is_orig: bool, options: vector of count) + { + print "requested options", options; + local vec: vector of string; + for ( _, o in options ) + vec += option_types[o]; + + c$dhcpv6_state$info$client_requested_options = vec; + } + +event dhcpv6_option_ia_na(c: connection, is_orig: bool, iaid: count, t1: interval, t2: interval) + { + local state = c$dhcpv6_state; + local info = state$info; + + # Weird? + if ( ! info?$ia_na ) + info$ia_na = IA_NA(); + + local ia_na = state$info$ia_na; + ia_na$iaid = iaid; + ia_na$t1 = t1; + ia_na$t2 = t2; + } + +event dhcpv6_option_ipaddr(c: connection, is_orig: bool, addr6: addr, preferred_lifetime: interval, valid_lifetime: interval) + { + local state = c$dhcpv6_state; + if ( c$dhcpv6_state$is_client ) + { + Weird::weird([$ts=network_time(), $uid=state$uid, $name="dhcpv6_ipaddr_option_from_client"]); + return; + } + + local info = state$info; + info$ia_na = IA_NA($iaaddr=addr6); + } + +event dhcpv6_option_client_fqdn(c: connection, is_orig: bool, n: bool, o: bool, s: bool, + domain_name: string) + { + print "GGGGGRR client fqdn", "n", n, "o", o, "s", s, domain_name; + } + +event dhcpv6_message_end(c: connection, is_orig: bool, msg_type: count, transaction_id: count) + { + print "dhcpv6_message_end", c$uid, c$id, is_orig; + +# TODO: Cluster, publish to manager + event aggregate_msgs(c$dhcpv6_state); + + delete c$dhcpv6_state; + } + event zeek_done() &priority=-5 { # Log any remaining data that hasn't already been logged! # for ( i in DHCP::join_data ) # join_data_expiration(DHCP::join_data, i); } + + +hook log_policy(rec: Info, id: Log::ID, filter: Log::Filter) + { + print "log_policy", rec; + } diff --git a/src/analyzer/protocol/dhcpv6/dhcpv6.evt b/src/analyzer/protocol/dhcpv6/dhcpv6.evt index 609753c952..5f9916155e 100644 --- a/src/analyzer/protocol/dhcpv6/dhcpv6.evt +++ b/src/analyzer/protocol/dhcpv6/dhcpv6.evt @@ -8,4 +8,20 @@ protocol analyzer DHCPv6 over UDP: import DHCPv6; -on DHCPv6::Message -> event dhcpv6_message($conn, $is_orig); +on DHCPv6::Message::transaction_id -> event dhcpv6_message($conn, $is_orig, cast(self.msg_type), self.transaction_id); + +on DHCPv6::Option if (self.top_level) -> event dhcpv6_option($conn, $is_orig, cast(self.code)); +on DHCPv6::Option::client_id if (self.top_level) -> event dhcpv6_option_clientid($conn, $is_orig, cast(self.client_id.duid_type), self.client_id.data); +on DHCPv6::Option::server_id if (self.top_level) -> event dhcpv6_option_serverid($conn, $is_orig, cast(self.server_id.duid_type), self.server_id.data); + +on DHCPv6::Option::status_code if (self.top_level) -> event dhcpv6_option_status_code($conn, $is_orig, cast(self.status_code.code), self.status_code.message); + +on DHCPv6::Option::request_option if (self.top_level) -> event dhcpv6_option_requested_options($conn, $is_orig, self.request_option.requested_options); + + +on DHCPv6::IA_NAOption -> event dhcpv6_option_ia_na($conn, $is_orig, self.iaid, interval(self.t1), interval(self.t2)); +on DHCPv6::IAADDROption -> event dhcpv6_option_ipaddr($conn, $is_orig, self.addr6, interval(self.preferred_lifetime), interval(self.valid_lifetime)); +on DHCPv6::ClientFQDNOption -> event dhcpv6_option_client_fqdn($conn, $is_orig, self.flags.n, self.flags.o, self.flags.s, self.domain_name); + + +on DHCPv6::Message::done -> event dhcpv6_message_end($conn, $is_orig, cast(self.msg_type), self.transaction_id); diff --git a/src/analyzer/protocol/dhcpv6/dhcpv6.spicy b/src/analyzer/protocol/dhcpv6/dhcpv6.spicy index a97952f305..e38fde1df2 100644 --- a/src/analyzer/protocol/dhcpv6/dhcpv6.spicy +++ b/src/analyzer/protocol/dhcpv6/dhcpv6.spicy @@ -26,7 +26,7 @@ type MessageType = enum { INFORMATION_REQUEST = 11, }; -type DDUIDType = enum { +type DUIDType = enum { LLT = 1, # Link-Layer Address Plus Time https://datatracker.ietf.org/doc/html/rfc8415#section-11.2 EN = 2, # Enterprise Number https://datatracker.ietf.org/doc/html/rfc8415#section-11.3 LL = 3, # Link-Layer Address https://datatracker.ietf.org/doc/html/rfc8415#section-11.4 @@ -54,13 +54,15 @@ type DUIDOption_UUID = unit { }; type DUIDOption = unit { - duid_type: uint16 &convert=DDUIDType($$); + duid_type: uint16 &convert=DUIDType($$); + data: bytes &eod; + switch (self.duid_type) { - DDUIDType::LLT -> llt: DUIDOption_LLT; - DDUIDType::EN -> en: DUIDOption_EN; - DDUIDType::LL -> ll: DUIDOption_LL; - DDUIDType::UUID -> uuid: DUIDOption_UUID; - }; + DUIDType::LLT -> llt: DUIDOption_LLT; + DUIDType::EN -> en: DUIDOption_EN; + DUIDType::LL -> ll: DUIDOption_LL; + DUIDType::UUID -> uuid: DUIDOption_UUID; + } &parse-from=self.data; }; type StatusCode = enum { @@ -82,14 +84,44 @@ type IA_NAOption = unit { iaid: uint32; t1: uint32; # seconds t2: uint32; # seconds - options: Option[] &eod; + options: Option(False)[] &eod; }; type IAADDROption = unit { - addr_: addr &ipv6; + addr6: addr &ipv6; preferred_lifetime: uint32; valid_lifetime: uint32; - options: Option[] &eod; + options: Option(False)[] &eod; +}; + +type RequestOption = unit { + requested_options: uint16[] &eod; +}; + +# DNS Label without compression. +# https://datatracker.ietf.org/doc/html/rfc1035#section-3.1 +type Label = unit { + len: uint8 &requires=(($$ & 0xC0) == 0); + label: bytes &size=self.len; +} &convert=self.label; + +# https://datatracker.ietf.org/doc/html/rfc4704#section-4 +type ClientFQDNOption = unit { + var domain_name: bytes; + flags: bitfield(8) { + mbz: 0..4; + n: 5 &convert=($$ == 1); + o: 6 &convert=($$ == 1); + s: 7 &convert=($$ == 1); + } &bit-order=spicy::BitOrder::MSB0; + + labels: Label[] &eod { + self.domain_name = b".".join($$); + } + + on %done { + print "YYYYYYY", self.flags, self.domain_name; + } }; type OptionCode = enum { @@ -98,10 +130,13 @@ type OptionCode = enum { IA_NA = 3, IA_TA = 4, IAADDR = 5, + REQUEST = 6, STATUS_CODE = 13, + CLIENT_FQDN = 39, }; -type Option = unit { +type Option = unit(top_level: bool) { + var top_level: bool = top_level; code: uint16 &convert=OptionCode($$); len: uint16; @@ -111,7 +146,9 @@ type Option = unit { OptionCode::IA_NA -> ia_na: IA_NAOption; # IA_TA OptionCode::IAADDR -> iaaddr: IAADDROption; + OptionCode::REQUEST -> request_option: RequestOption; OptionCode::STATUS_CODE -> status_code: StatusCodeOption; + OptionCode::CLIENT_FQDN -> client_fqdn: ClientFQDNOption; * -> unknown: bytes &eod; } &size=self.len; }; @@ -127,13 +164,15 @@ public type Message = unit { transaction_id: uint8[3] &convert=uint32((uint32($$[0]) << 16) + (uint32($$[1]) << 8) + uint32($$[2])); - options: Option[] &eod; + options: Option(True)[] &eod; # Once the options are parsed, what do we actually want to # send to script land? Maybe a vector with options that have # a lot of optionals? + done: void; on %done { + return; print self.msg_type, self.transaction_id; for (o in self.options) { print o;