The broadband access network I work with relies on DHCP Option-82 to manage DHCP leases, among other things.
The primary access equipment vendor (Cambium — a spinoff of Motorola) is unfriendly, in that the Option-82 circuit-id (i.e. Access Point) and remote-id (i.e. Subscriber Module) fields are binary representations of the respective network element’s MAC addresses. Even `tcpdump’ has a hard time reading it:
Remote-ID SubOption 2, length 6: ^J^@>8ZM-`
That poses certain problems. For one, creating a static IP assignment (based on the customer SM’s MAC address, which is Option 82 remote-id) gets ugly in dhcpd.conf file. For example:
match if binary-to-ascii(16, 8, ":", suffix( option agent.remote-id, 6)) = "a:0:3e:38:5a:e0"
… is a lot harder to read than
match if option agent.remote-id = "a:0:3e:38:5a:e0"
But since agent.remote-id is a binary-encoded MAC address, rather than human-readable string, the binary-to-ascii conversion is necessary. And, since the binary-to-ascii function strips leading zeros, we have to revisit a bunch of existing code that tracks CPE MAC addresses. What I wanted was a way to convert the remote-id (SM’s MAC address) and circuit-id (AP’s MAC address) to a human-readable MAC addresses, to make dhcpd.conf file easier to read and script.
UPDATE: It turns out I could indeed match the agent.remote-id using a standard represtation of a MAC address for the remote-id. I just needed to leave out the quotes! For example:
match if option agent.remote-id = 0a:00:3e:38:5a:e0
Had I realized that, I never would have bothered with the decoding the Option 82 vendor string and all the other work I describe below. Oh well… at least I learned something.
Because the DHCP server may be used for other vendors (i.e. GPON, etc), I wanted to only do these manipulations of the Option-82 data fields that are set by the Motorola/Cambium radios. Fortunately, Cambium AP’s also set Option 82 Subclass 9, which a vendor specific entry.
While `tcpdump’ doesn’t understand 82.9:
Unknown SubOption 9, length 11: 0x0000: 0000 00a1 0613 0401 020b b8
… Wireshark does!
Option 82 Suboption: (9) Vendor-Specific Information Length: 11 Enterprise: Motorola (161) Data Length: 6 Value: 130401020bb8 Hex: 090b000000a106130401020bb8
Reading this:
82.9.1 is the 4-byte Vendor ID (i.e. IANA Enterprise assignments, like what is used in SNMP MIB’s)
82.9.2 is an unspecified field that is 6-bytes long in this case (I can’t figure out what, if anything, it translates too)
Unfortunately, as of dhcpd 4.3.1, there is no “option agent.vendor-id” field available — out of the 12 DHCP Option-82 subcodes currently defined, ISC’s man page for `dhcp-options` only lists four possibilities for agent matching:
- 82.1: option agent.circuit-id
- 82.2: option agent.remote-id
- 82.4: option agent.DOCSIS-device-class
- 82.5: option agent.link-selection
Fortunately, I found that the vendor (subcode 9) options are available — it’s just not documented. But if you know where to look, you can find that the undocumented ‘option agent.unknown-9’ field contains the raw bits of the vendor extension field. This seems easier than this workaround somebody else discovered — plus it doesn’t destory the ‘option agent.*’ information that’s used for the ‘stash-agent-option’ config flag.
Using ‘option agent.unknown-9’, I was able to grab the vendor ID (what Wireshark decoded as 161 — Motorola) in dhcpd.conf:
set myvendor = binary-to-ascii(10, 16, ",", substring(option agent.unknown-9,2,2));
Putting it all together:
# when the DHCP server gets an initial request, it contains Option 82 information # that is added by a network element (i.e. from the AP, OLT, CMTS, DSLAM, switch, etc). # however, subsequent renewals are unicasted from the subscriber to the DHCP server. # those unicasted requests don't get Option 82 values added by the network element. # because we will match on agent options, we need to stash this info so that renewals # will work as expected. basically, this will create a database linking the hardware address # to the Option 82 values that were found in the initial DHCP DISCOVER packet stash-agent-option; # DHCP Option 82 *may* contain a vendor subtype (code 9) # ISC-DHCPD knows it's there, but doesn't know how to decode it if exists agent.unknown-9 { # VENDOR definition set myvendor = binary-to-ascii(10, 16, ",", substring(option agent.unknown-9,2,2)); # For customers behind Motorola (i.e. Cambium SM), format ciruit-id and remote-id as MAC's if myvendor = "161" # Motorola { # SM definition # we want to restore the leading zero's that get stripped when converting binary to ascii set mysm = concat ( suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,0,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,1,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,2,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,3,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,4,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,5,1))),2) ); # AP definition # we want to restore the leading zero's that get stripped when converting binary to ascii set myap = concat ( suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.circuit-id,0,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.circuit-id,1,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.circuit-id,2,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.circuit-id,3,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.circuit-id,4,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.circuit-id,5,1))),2) ); # add a line to syslog showing the IP -> SM -> AP mapping log ( info, concat ( "WISP IPv4 ", binary-to-ascii (10, 8, ".", leased-address), " SM " , mysm, " AP ", myap, " VENDOR ", myvendor)); } } # to simplify later 'deny' rules, track all static customers here class "static-sm" { # unfortunately, I can't just do a "match mysm" here, so I have to redo my string manipulation on the "option agent.remote-id" match concat ( suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,0,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,1,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,2,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,3,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,4,1))),2), ":", suffix (concat ("0", binary-to-ascii (16, 8, "", substring(option agent.remote-id,5,1))),2) ); } # each static-IP subscriber gets a "static-sm" subclass entry like this subclass "static-sm" "0a:00:3e:38:5a:e0"; # each static-IP subscriber gets a unique class with just their SM's MAC class "alk-static" { match if mysm = "0a:00:3e:38:5a:e0"; } # BRAS/BNG Testing Subnets shared-network brastest-default { subnet 192.168.1.0 netmask 255.255.255.0 { option routers 192.168.1.254; } pool { # this is the static IP assignment for "alk-static" # each static IP customer gets a pool definition like this one allow members of "alk-static"; allow known-clients; range 192.168.1.200; } pool { # this is the default pool for dynamic customers deny dynamic bootp clients; # DHCP is OK, BOOTP is not... deny members of "static-sm"; # List of all static assignments (subclasses) range 192.168.1.1 192.168.1.199; } }