Thursday, May 9, 2013

ISC DHCPD programming fun: (attempting to) install routes following DHCP-PD operation

For testing, I need to run both the DHCPv6 server and the router on the single box, with the CPEs running behind it doing client DHCP-PD requests.

As such box I am using a Linksys E3200 running TomatoUSB + with optware aiccu in order to flexibly get the globally routable IPv6 chunk, and optware dhcpd to do all the DHCPv6 stuff.

Configuring DHCP-PD on the ISC DHCP was a breeze, but the tricky part is routing the traffic - DHCP-PD implies the prefix is behind a certain CPE, which implies a route... Some searching pointed to http://jpmens.net/2011/07/06/execute-a-script-when-isc-dhcp-hands-out-a-new-lease/ - but it seems to be a no-op with IPv6... duh. However, Wim's (@42wim) blog entry at http://blog.42.be/2013/04/dhcpv6-isc-mac-logging-and-cisco-relay.html hinted at another approach... 

The result is the below ugly hack, which might be enough for my needs (I won't change the CPEs too often and they are all OpenWRT so the initial 5 minutes delay should be fine; and I do not look at the security aspect -  looks like this is just blindly installing routes based on the info supplied by the client in the REBIND, which in any real-world deployment would be a no-no... 

Also since I derive the link-local address from the mac address that I derive in a way similar to Wim's approach - this is also very very fragile, but again - for my toy setup it is enough. 

The funniest part was the U/L bit flipping while converting the MAC address into the Link-Local IPv6 address. Luckily the ISC DHCP implements just enough logic for it to work. (Though I would have liked a decent programming language there... Maybe ISC folks could just hook Lua inside ? I suspect this might actually decrease the amount of code and with something like e.g. Lua 5.1 the bugs are very rare if at all.)

If someone has a better idea on how to implement this, feel free to write in the comment. My scenario constraints are:

1) the ISC DHCP server is whatever is supplied with tomatousb firmware, I believe 4.1 - I can not recompile or upgrade it to anything newer.

2) I do not have any cross-compiler toolchain for tomatousb

So I am pretty restricted in what can be done.

Anyway,  here goes my dhcp.conf for your enjoyment:

# BIIIG caveat:
# * does not work on initial allocation.
#   looks like we can only study the options sent by the client, 
#   not the options about to be sent by the server, so...
#   
# Some assumptions:
# * DUID has mac address in the end (openwrt does this)
# * only one prefix in PD, and one address in NA. This is a bit buggy
default-lease-time 600;
max-lease-time 7200; 
log-facility local7; 

 # thanks @42Wim! :-)

 option dhcp6.macaddr code 193 = string;
 option dhcp6.leased-address code 194 = string;
 option dhcp6.pkt code 9998 = string;
 option dhcp6.leased-prefix code 9999 = string;
 option dhcp6.leased-prefix-len code 9997 = string;
 option dhcp6.ll-addr code 9996 = string;
 option dhcp6.leased-prefix-cidr code 9995 = string;
 
 option dhcp6.uli code 6011 = integer 32;
 option dhcp6.ulo code 6010 = integer 32;
 
 option dhcp6.macaddr = binary-to-ascii(16, 8, ":", 
                                        suffix(option dhcp6.client-id, 6));
 
 # extract the byte with U/L bit
 option dhcp6.uli = substring(suffix(option dhcp6.client-id, 6), 0, 1);

 # invert the U/L bit by checking the symbol in the binary string 
 # representation and adjusting the result accordingly
 if substring(suffix(binary-to-ascii(2, 8, "",
                                  config-option dhcp6.uli), 2), 0, 1) = "1" {
   # Seems there's no minus so we gotta do subtraction by addition overflow
   option dhcp6.ulo = encode-int(
                         extract-int(config-option dhcp6.uli, 8) + 254, 8);
 } else {
   option dhcp6.ulo = encode-int(
                         extract-int(config-option dhcp6.uli, 8) + 2, 8);
 }
 
 option dhcp6.ll-addr = concat("fe80::", binary-to-ascii(16,16, ":", 
                                 concat(config-option dhcp6.ulo, 
                                        substring(suffix(option dhcp6.client-id, 6), 1, 2), 
                                        encode-int(255, 8),
                                        encode-int(254, 8),
                                        substring(suffix(option dhcp6.client-id, 6), 3, 3))) );
 
 
 option dhcp6.leased-prefix = binary-to-ascii(16,16, ":",
                                suffix(substring(option dhcp6.ia-pd, 12, 100), 16));
 option dhcp6.leased-prefix-len = binary-to-ascii(10,8, ".",
                                    substring(suffix(substring(option dhcp6.ia-pd, 12, 100), 17), 0, 1));
 option dhcp6.leased-prefix-cidr = concat (config-option dhcp6.leased-prefix, "/", 
                                           config-option dhcp6.leased-prefix-len);
 
 if substring(config-option dhcp6.leased-prefix, 0, 1) = "2" {
   log (info, concat ("Prefix ",config-option dhcp6.leased-prefix-cidr, 
                      " leased to ", config-option dhcp6.macaddr, 
                      " via ", config-option dhcp6.ll-addr));
   execute("/usr/sbin/ip", "-6", "route", "del", config-option dhcp6.leased-prefix-cidr);
   execute("/usr/sbin/ip", "-6", "route", "add", config-option dhcp6.leased-prefix-cidr, 
                                                 "via", config-option dhcp6.ll-addr, "dev", "br0");
   
 } else {
   log (info, "No prefix leased in this packet or not REBIND");
 }
 
# pretty standard stuff.

subnet6 2001:6f8:147e::/64 {
        # Range for clients
        range6 2001:6f8:147e::1000 2001:6f8:147e::ffff;
        
        # Additional options
        option dhcp6.name-servers 2001:6f8:147e::1;
        option dhcp6.domain-search "domain.example";

        # Prefix range for delegation to sub-routers
        prefix6 2001:6f8:147e:1100:: 2001:6f8:147e:1f00::  /56;
}