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:
1 | 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:
1 | 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
1 | 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:
1 | 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:
1 2 | Unknown SubOption 9, length 11: 0x0000: 0000 00a1 0613 0401 020b b8 |
… Wireshark does!
1 2 3 4 5 6 7 | 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:
1 | set myvendor = binary-to-ascii(10, 16, ",", substring(option agent.unknown-9,2,2)); |
Putting it all together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | # 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; } } |