ISC DHCPD – Putting Option-82 Vendor Codes To Use To Reformat Cirtuit-ID and Remote-ID Values

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;
}
}