{"id":198,"date":"2015-10-09T14:13:47","date_gmt":"2015-10-09T19:13:47","guid":{"rendered":"https:\/\/adamkuj.net\/blog\/?p=198"},"modified":"2021-05-11T08:59:49","modified_gmt":"2021-05-11T13:59:49","slug":"isc-dhcpd-putting-option-82-vendor-codes-to-use","status":"publish","type":"post","link":"https:\/\/adamkuj.net\/blog\/2015\/10\/09\/isc-dhcpd-putting-option-82-vendor-codes-to-use\/","title":{"rendered":"ISC DHCPD &#8211; Putting Option-82 Vendor Codes To Use To Reformat Cirtuit-ID and Remote-ID Values"},"content":{"rendered":"<p>The broadband access network I work with relies on DHCP Option-82 to manage DHCP leases, among other things.<\/p>\n<p>The primary access equipment vendor (Cambium &#8212; 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&#8217;s MAC addresses. Even `tcpdump&#8217; has a hard time reading it:<\/p>\n<p>[code]Remote-ID SubOption 2, length 6: ^J^@&gt;8ZM-`[\/code]<\/p>\n<p>That poses certain problems. For one, creating a static IP assignment (based on the customer SM&#8217;s MAC address, which is Option 82 remote-id) gets ugly in dhcpd.conf file. For example:<\/p>\n<p>[code]match if binary-to-ascii(16, 8, &#8220;:&#8221;, suffix( option agent.remote-id, 6)) = &#8220;a:0:3e:38:5a:e0&#8243;[\/code]<\/p>\n<p>&#8230; is a lot harder to read than<\/p>\n<p>[code]match if option agent.remote-id = &#8220;a:0:3e:38:5a:e0&#8243;[\/code]<\/p>\n<p>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&#8217;s MAC address) and circuit-id (AP&#8217;s MAC address) to a human-readable MAC addresses, to make dhcpd.conf file easier to read and script.<\/p>\n<p><strong>UPDATE:<\/strong> <em>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:<\/em><\/p>\n<p>[code]match if option agent.remote-id = 0a:00:3e:38:5a:e0[\/code]<\/p>\n<p><em>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&#8230; at least I learned something.<\/em><\/p>\n<p>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&#8217;s also set Option 82 Subclass 9, which a vendor specific entry.<\/p>\n<p>While `tcpdump&#8217; doesn&#8217;t understand 82.9:<\/p>\n<p>[code]Unknown SubOption 9, length 11:<br \/>\n0x0000: 0000 00a1 0613 0401 020b b8[\/code]<\/p>\n<p>&#8230; Wireshark does!<\/p>\n<p>[code]Option 82 Suboption: (9) Vendor-Specific Information<br \/>\nLength: 11<br \/>\nEnterprise: Motorola (161)<br \/>\nData Length: 6<br \/>\nValue: 130401020bb8<\/p>\n<p>Hex: 090b000000a106130401020bb8[\/code]<\/p>\n<p>Reading this:<\/p>\n<p>82.9.1 is the 4-byte Vendor ID (i.e. IANA Enterprise assignments, like what is used in SNMP MIB&#8217;s)<\/p>\n<p>82.9.2 is an unspecified field that is 6-bytes long in this case (I can&#8217;t figure out what, if anything, it translates too)<\/p>\n<p>Unfortunately, as of dhcpd 4.3.1, there is no &#8220;option agent.vendor-id&#8221; field available &#8212; out of the 12 <a title=\"BOOTP\/DHCP option 82, Relay Agent Information\" href=\"http:\/\/www.networksorcery.com\/enp\/protocol\/bootp\/option082.htm\" target=\"_blank\" rel=\"noopener noreferrer\">DHCP Option-82 subcodes currently defined<\/a>, ISC&#8217;s man page for `dhcp-options` only lists four possibilities for agent matching:<\/p>\n<ul>\n<li><span style=\"line-height: 1.714285714; font-size: 1rem;\">82.1: option agent.circuit-id<\/span><\/li>\n<li><span style=\"line-height: 1.714285714; font-size: 1rem;\">82.2: option agent.remote-id<\/span><\/li>\n<li><span style=\"line-height: 1.714285714; font-size: 1rem;\">82.4: option agent.DOCSIS-device-class<\/span><\/li>\n<li><span style=\"line-height: 1.714285714; font-size: 1rem;\">82.5: option agent.link-selection<\/span><\/li>\n<\/ul>\n<p>Fortunately, I found that the vendor (subcode 9) options are available &#8212; it&#8217;s just not documented. But if you know where to look, you can find that the undocumented &#8216;option agent.unknown-9&#8217; field contains the raw bits of the vendor extension field. This seems easier than this <a title=\"\" href=\"https:\/\/lists.isc.org\/pipermail\/dhcp-users\/2011-October\/014131.html\" target=\"_blank\" rel=\"noopener noreferrer\">workaround<\/a> somebody else discovered &#8212; plus it doesn&#8217;t destory the &#8216;option agent.*&#8217; information that&#8217;s used for the &#8216;stash-agent-option&#8217; config flag.<\/p>\n<p>Using &#8216;option agent.unknown-9&#8217;, I was able to grab the vendor ID (what Wireshark decoded as 161 &#8212; Motorola) in dhcpd.conf:<\/p>\n<p>[code]set myvendor = binary-to-ascii(10, 16, &#8220;,&#8221;, substring(option agent.unknown-9,2,2));[\/code]<\/p>\n<p>Putting it all together:<\/p>\n<p>[shell]<br \/>\n# when the DHCP server gets an initial request, it contains Option 82 information<br \/>\n# that is added by a network element (i.e. from the AP, OLT, CMTS, DSLAM, switch, etc).<br \/>\n# however, subsequent renewals are unicasted from the subscriber to the DHCP server.<br \/>\n# those unicasted requests don&#8217;t get Option 82 values added by the network element.<br \/>\n# because we will match on agent options, we need to stash this info so that renewals<br \/>\n# will work as expected. basically, this will create a database linking the hardware address<br \/>\n# to the Option 82 values that were found in the initial DHCP DISCOVER packet<br \/>\nstash-agent-option;<\/p>\n<p># DHCP Option 82 *may* contain a vendor subtype (code 9)<br \/>\n# ISC-DHCPD knows it&#8217;s there, but doesn&#8217;t know how to decode it<br \/>\nif exists agent.unknown-9<br \/>\n{<\/p>\n<p># VENDOR definition<br \/>\nset myvendor = binary-to-ascii(10, 16, &#8220;,&#8221;, substring(option agent.unknown-9,2,2));<\/p>\n<p># For customers behind Motorola (i.e. Cambium SM), format ciruit-id and remote-id as MAC&#8217;s<br \/>\nif myvendor = &#8220;161&#8221; # Motorola<br \/>\n{<br \/>\n# SM definition<br \/>\n# we want to restore the leading zero&#8217;s that get stripped when converting binary to ascii<br \/>\nset mysm = concat (<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,0,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,1,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,2,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,3,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,4,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,5,1))),2)<br \/>\n);<\/p>\n<p># AP definition<br \/>\n# we want to restore the leading zero&#8217;s that get stripped when converting binary to ascii<br \/>\nset myap = concat (<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.circuit-id,0,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.circuit-id,1,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.circuit-id,2,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.circuit-id,3,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.circuit-id,4,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.circuit-id,5,1))),2)<br \/>\n);<\/p>\n<p># add a line to syslog showing the IP -&gt; SM -&gt; AP mapping<br \/>\nlog ( info, concat (<br \/>\n&#8220;WISP IPv4 &#8220;, binary-to-ascii (10, 8, &#8220;.&#8221;, leased-address),<br \/>\n&#8221; SM &#8221; , mysm,<br \/>\n&#8221; AP &#8220;, myap,<br \/>\n&#8221; VENDOR &#8220;, myvendor));<br \/>\n}<\/p>\n<p>}<\/p>\n<p># to simplify later &#8216;deny&#8217; rules, track all static customers here<br \/>\nclass &#8220;static-sm&#8221; {<br \/>\n# unfortunately, I can&#8217;t just do a &#8220;match mysm&#8221; here, so I have to redo my string manipulation on the &#8220;option agent.remote-id&#8221;<br \/>\nmatch concat (<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,0,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,1,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,2,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,3,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,4,1))),2), &#8220;:&#8221;,<br \/>\nsuffix (concat (&#8220;0&#8221;, binary-to-ascii (16, 8, &#8220;&#8221;, substring(option agent.remote-id,5,1))),2)<br \/>\n);<br \/>\n}<\/p>\n<p># each static-IP subscriber gets a &#8220;static-sm&#8221; subclass entry like this<br \/>\nsubclass &#8220;static-sm&#8221; &#8220;0a:00:3e:38:5a:e0&#8221;;<\/p>\n<p># each static-IP subscriber gets a unique class with just their SM&#8217;s MAC<br \/>\nclass &#8220;alk-static&#8221; {<br \/>\nmatch if mysm = &#8220;0a:00:3e:38:5a:e0&#8221;;<br \/>\n}<\/p>\n<p># BRAS\/BNG Testing Subnets<br \/>\nshared-network brastest-default {<br \/>\nsubnet 192.168.1.0 netmask 255.255.255.0 {<br \/>\noption routers 192.168.1.254;<br \/>\n}<br \/>\npool {<br \/>\n# this is the static IP assignment for &#8220;alk-static&#8221;<br \/>\n# each static IP customer gets a pool definition like this one<br \/>\nallow members of &#8220;alk-static&#8221;;<br \/>\nallow known-clients;<br \/>\nrange 192.168.1.200;<br \/>\n}<br \/>\npool {<br \/>\n# this is the default pool for dynamic customers<br \/>\ndeny dynamic bootp clients; # DHCP is OK, BOOTP is not&#8230;<br \/>\ndeny members of &#8220;static-sm&#8221;; # List of all static assignments (subclasses)<br \/>\nrange 192.168.1.1 192.168.1.199;<br \/>\n}<br \/>\n}<br \/>\n[\/shell]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 &#8212; 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&#8217;s MAC addresses. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[25,24,29,28,26,27,30],"class_list":["post-198","post","type-post","status-publish","format-standard","hentry","category-networking-tools","tag-bng","tag-bras","tag-cambium","tag-canopy","tag-dhcp","tag-dhcpd","tag-ipoe"],"_links":{"self":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts\/198","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/comments?post=198"}],"version-history":[{"count":19,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts\/198\/revisions"}],"predecessor-version":[{"id":314,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts\/198\/revisions\/314"}],"wp:attachment":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/media?parent=198"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/categories?post=198"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/tags?post=198"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}