Showing posts with label dhcp. Show all posts
Showing posts with label dhcp. Show all posts

Monday, 20 April 2015

DHCP, RPF verify, FHRP and ECMP - when protocols collide

I've been gradually enabling ECMP (Equal-Cost MultiPath) routing on parts of the University network as part of a prelude to the new data centre network and to improve performance generally (by making use of the dual downstream interfaces into institutions).

For the most part, this is fairly straightforward and just worked - I'll explain what was needed in each case in a future post (as MPLS VPN works slightly differently and needs some special consideration), but I did get a nasty problem which broke DHCP relaying in some places that I'll cover here.

How DHCP relaying works


First, let's review how DHCP relaying works (what you get when you do ip helper-address ... on a Cisco router interface towards clients).  Consider the following network:
When a client on the subnet 192.168.1.0/24 wants to DHCP, the following happens initially:
  1. The client sends out a DHCP DISCOVER message from 0.0.0.0 (as it doesn't know its IP address or even subnet, yet) on UDP port 68 (the DHCP client port) to all-hosts broadcast (255.255.255.255) port 67 (DHCP server).
  2. The DHCP relay agent (which is normally the router) will receive this broadcast and forward it as a unicast packet to the DHCP server listed in the ip helper-address ... interface command.  This will be from the router's interface address (this is the critical bit) - 192.168.1.253, in the above example, port 67 to the DHCP server, port 67.
  3. The DHCP server will receive the DISCOVER and, assuming it has an address (and other information) to give to the client, it will send a DHCP OFFER message back to the router to be relayed onto it.  This will be the reverse of the packet just received: going from the DHCP server's address, port 67 to the router's interface address, port 67.
  4. The router will receive the reply and unicast it back to the client, sending it from its interface IP address, port 67 to the client's prospective IP address, port 68.
This all works fine and, assuming the client wants to take the address, it will send a DHCP REQUEST (using the same process) and receive a DHCP ACK back from the server so it can begin using the address.

How DHCP relaying doesn't work


Now consider what happens when the same DHCP DISCOVER is relayed by the other client subnet router, without ECMP:
Here, something goes wrong at step 3, trying to return the DHCP OFFER to the relaying router:
  1. Because the backbone network (depicted with the cloud symbol) only knows about routing to the client subnet as a whole /24 (not the individual routers' addresses), it routes the packet via 192.168.1.253.
  2. 192.168.1.253 treats 192.168.1.252 as just another host on the client subnet and forwards it out of its interface onto that subnet.
  3. Because 192.168.1.252 has anti-spoofing blocks in place on the client subnet interface, it rejects this packet as the source address is that of the DHCP server: an invalid address from the client subnet.
With this configuration, typical for a network with a FHRP (First Hop Redundancy Protocol) such as HSRP or VRRP is in use, half of the DHCP replies (the ones relayed via one of the two routers) will be lost when they're returned to the relaying routers.

However, this in itself isn't particularly a problem in that both routers will relay the same packet to the DHCP server, resulting it receiving two copies of each DISCOVER [but with different relay agent / forwarder addresses], causing two OFFERs to be returned; the clients will not miss out as they'll get one of the two copies.  This is why I've never noticed this problem, even though it's been going on for years: the clients have still got their address and worked.

(Actually, I was sort-of aware of this problem, as it prevented pinging one of the routers' own addresses on a particular interface, if the source of the ping was elsewhere on the network.  However, that's just been a minor inconvenience and not service-affecting; I never realised that would also be affecting DHCP.)

Combining with ECMP


When this situation is combined with ECMP this can get messy: the returned DHCP OFFERs (and ACKs) might be returned to either of the two client subnet routers.  The routers' addresses are often 1 number offset (e.g. 192.168.1.252 vs .253) which will likely mean they each take a different path.

If the path for packets to the .253 relay address happen to go directly to the .253 router, all is fine.  Same with .252.

However, if you're really unlucky (and, of course, we were, in some situations), ECMP will return the .253 packet via the .252 router and the .252 packet via .253.  This results in both replies being rejected and the client getting neither of the responses.

Fixing the problem and creating another


I couldn't find any way to direct the replies back to the correct router (e.g. by advertising the router's interface IP address into OSPF as a /32), so dealing with them being rejected by the anti-spoofing protection seemed the only solution.

As I've written, I've been looking at the ip verify unicast source ... command recently, and it seemed a good opportunity to employ that, rather than modify lots of access control lists.  According to Cisco's documentation, that command has a special feature in to handle DHCP:
"Unicast RPF will allow packets with 0.0.0.0 source and 255.255.255.255 destination to pass so that Bootstrap Protocol (BOOTP) and Dynamic Host Configuration Protocol (DHCP) functions work properly."
— from the IOS Security Configuration Guide for IOS 12.2SX
Sounds good, except that doesn't handle the source addresses of relayed DHCP replies.  It would be nice if this included "packets with a source address of the interface ip helper-address and port 67, destined for the router's interface address port 67", but it doesn't.

However, the command has a feature to allow packets matching an access list to be accepted, even if they fail the RPF check.  It's configured as follows:

ip access-list extended 1301
 permit udp host DHCP-SERVER eq 67 192.168.1.0 0.0.0.255 eq 67
!
interface ...
 ip address 192.168.1.253 255.255.255.0
 standby ip 192.168.1.254
 ip verify unicast source reachable-via rx 1301

This will allow the initial DHCP DISCOVER in (as described in the Cisco documentation), regular 192.168.1.0/24 traffic (due to the RPF check) AND traffic from the DHCP server on port 67 to an address on the same subnet port 67 (which is less tedious than putting the interface IP address itself as it can be copied to the other router without modification).  This change can be combined with a simplification of the interface access lists (if used).  So, I implemented a few of these and all looked hunky dory.

The IP Input process - my old nemesis


However, a little while later, we started getting alarms for CPU usage on the Catalyst 6500-E routers.  A show process cpu sorted command showed high load caused by the IP Input process.

This is usually caused by excessive traffic being forwarded ("punted" in Cisco parlance) to the Route Processor (RP) for handling.  We can capture and display these with the following commands:

router# debug netdr capture rx
router# show netdr captured-packets

(Use debug netdr clear-capture to clear the buffer and our old friend undebug all to switch it off.)

The packets being punted all appeared to be regular data - nothing complicated like DHCP which needs special processing, so I started doing some more reading and found a document on Cisco's website explaining how this configuration is handled on a 6500:
"For unicast RPF check without ACL filtering, the PFC3 provides hardware support for the RPF check of traffic from multiple interfaces. 
For unicast RPF check with ACL filtering, the PFC determines whether or not traffic matches the ACL. The PFC sends the traffic denied by the RPF ACL to the route processor (RP) for the unicast RPF check. Packets permitted by the ACL are forwarded in hardware without a unicast RPF check." 
— from the IOS Network Security guide for IOS 12.2SX on Catalyst 6500 with PFC3
So it appears that, when you use an ACL, all the traffic not matching the ACL will get punted to the RP.  Excellent.

Fixing the problem for good


So, I backtracked on using the ip verify unicast ... command and reverted to using our old inbound access lists to protect against address spoofing.  These now have an extra entry and look as follows:

ip access-list extended in-subnet
 permit ip 192.168.1.0 0.0.0.255 any
 permit udp any eq bootpc host 255.255.255.255 eq bootps
 permit udp host DHCP-SERVER eq bootps 192.168.1.0 0.0.0.255 eq bootps
 deny ip any any

This appears to do the trick and doesn't involve the RP on the router going bananas.  Given this problem, I think I'll abandon using the ip verify unicast ... command!

(Update 2018-02-01 — we have since installed Catalyst 6807-XLs with Supervisor 6Ts and came up with a final solution.  I've described that on a separate blog post.)

Monday, 16 September 2013

DHCP Option 82 update - Cisco and HP

Following the work on DHCP with Option 82 and Cisco switches and routers, I thought I'd summarise things and try getting it to work with HP ProCurve switches, too (by "ProCurve" I mean the "traditional" HP switches and not the ex-H3C ones running Comware - we don't have any of the latter).

The agent remote-id and circuit-id fields are left as vendor-specific and, of course, differ between switch platforms.  Detecting the different formats is a little hit-and-miss, but seems to be possible between HP and Cisco, at least.  When I get time later, I may look at Extreme XOS stuff as we use that too (although only in our data centres, where we don't have DHCP in use, except to set up servers initially).

HP DHCP Snooping and Option 82


HP has three modes for the remote-id, set with the dhcp-snooping option 82 remote-id global command: mac (the default) - just 6 bytes of the base MAC address of the switch, subnet-ip - the switch's IP address on the VLAN with the client (I have no idea what happens if there isn't one set), and mgmt-ip - the management IP address (the IP address set on the management VLAN).  The latter looks the most useful.  There is no leading byte to indicate which of these options has been selected.

There seems to be no way to control what the format of the circuit-id takes: on the switches I've tested it on - a 2610-24-PWR and a 5412), it's two bytes - the first is zero and I assume would increase with slot numbers on a chassis-based switch (the AP I have on the 5412 is in slot A and it's still 0); the second is the port number.

Cisco and HP Option 82 information compared


The remote-id is as follows:

Format01234567-
Cisco(default)0Switch base MAC address
Hostname1LenHostname or explicit string
HPmacSwitch base MAC address
subnet-ipClient VLAN IP address?
mgmt-ipManagement VLAN IP address

The logic to parse this field seems best as:
  1. If byte 0 is 1, it's probably a Cisco hostname (it's unlikely to be an IP address "1.q.r.s" or a multicast MAC address), so print the string starting at byte 2
  2. If it's 4 bytes long, assume it's an HP IP address, so print it as an IPv4 address
  3. Else assume it's an HP MAC address so print as colon-separate hex string (which, if it's not a MAC address, we can still translate)
The circuit-id is one of:

Format0123456-
Ciscovlan-mod-port04 (= Length)VLAN ID (big endian)Module (slot)Port
string1LengthPort and VLAN string
HP-SlotPort

Depending on what the remote-id was selected as, it seems best to print this as:

  • Cisco - parse the 2-byte VLAN ID and print the port as "module/port" in decimal
  • HP - print the data as hyphen-separated decimals (it could be separated by slashes, but this is just to differentiate)

Cisco configuration


The following is what I've used on the Cisco switches; the lines with a leading "!" are defaults:

ip dhcp snooping vlan ...
!ip dhcp snooping information option
ip dhcp snooping information option format remote-id hostname
!no ip dhcp snooping information option allow-untrusted
ip dhcp snooping database ...
ip dhcp snooping database write-delay 900
ip dhcp snooping
!
interface <uplink>
 ip dhcp snooping trust

On the Cisco routers, I've issued the following, to allow Option 82 to be set by a downstream switch:

ip dhcp relay information trust-all

HP configuration


The following is what I've used on HP switches; the lines with a leading "!" are defaults:

dhcp-snooping vlan ...
dhcp-snooping option 82 remote-id mgmt-ip
!dhcp-snooping option 82 untrusted-policy drop
!dhcp-snooping option 82
dhcp-snooping database file ...
dhcp-snooping
!
interface <uplink>
 dhcp-snooping trust

Printing the agent details in ISC DHCP


Adapting the code posted before to cope with these variations:

# if the agent (Option 82) details are present, attempt to read information
# from them; this is tricky because different vendors and configurations can
# return information in conflicting formats and it's difficult to work out
# what format the information is in, so we make some assumptions

if exists agent.remote-id {
  if substring(option agent.remote-id, 0, 1) = 1 {
    # the first byte of the remote ID is 1 - that's unlikely to be an IP addr-
    # ess and, if it were a MAC address it would be multicast, so it is
    # probably a hostname from a Cisco switch
    log(
      info,
      concat(
        "agent information ",
        binary-to-ascii(10, 8, ".", leased-address),
        " to ",
        binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)),
        " on ",
        substring(option agent.remote-id, 2, extract-int(substring(option agent.remote-id, 1, 1), 8)),
        " port ",
        binary-to-ascii(10, 8, "", substring(option agent.circuit-id, 4, 1)),
        "/",
        binary-to-ascii(10, 8, "", substring(option agent.circuit-id, 5, 1)),
        " VLAN ",
        binary-to-ascii(10, 16, "", substring(option agent.circuit-id, 2, 2))));

  } elsif substring(option agent.remote-id, 4, 2) = "" {
    # if the length of the remote ID is less than 6, we probably have an IP
    # address from an HP
    log(
      info,
      concat(
        "agent information ",
        binary-to-ascii(10, 8, ".", leased-address),
        " to ",
        binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)),
        " on ",
        binary-to-ascii(10, 8, ".", option agent.remote-id),
        " port ",
        binary-to-ascii(10, 8, "-", option agent.circuit-id)));

  } else {
    # otherwise, we probably have an HP MAC address, so just print the rem-
    # ID as comma-separated hex; this doesn't hurt for anything else, anyway,
    # as we can always translate it
    log(
      info,
      concat(
        "agent information ",
        binary-to-ascii(10, 8, ".", leased-address),
        " to ",
        binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)),
        " on ",
        binary-to-ascii(16, 8, ":", option agent.remote-id),
        " port ",
        binary-to-ascii(10, 8, "-", option agent.circuit-id)));
  }
}

This gives log output for an HP:

Sep 16 22:56:52 janganmun dhcpd: agent information 172.30.162.130 to 6c:f3:7f:c0:54:cf on 172.30.64.114 port 0-23

... the switch has management IP address 172.30.64.114 and the port is A23 (on a 5412).

Or, for a Cisco:

Sep 16 23:08:23 janganmun dhcpd: agent information 172.30.140.9 to 24:de:c6:c6:51:40 on sw-ucs-rnb-n3 port 2/37 VLAN 3008

... switch sw-ucs-rnb-n3, port Gi2/0/37 on VLAN 3008.

Saturday, 14 September 2013

DHCP Option 82, Cisco switches and routers and the ISC DHCP server

A bit of "fun" with IPv6 multicast this week which I'll come onto in another post...

We're moving into a new building beginning on Monday and our new network makes heavy use of pooled DHCP (with dynamic allocations of private addresses), and an option of fixed private or public addresses for devices needing fixed registrations and/or access from outside the University network.  The dynamic pooled allocations wouldn't need to be registered, enabling odd machines to be brought onto the network more easily, saving the fixed registrations for things that really need them.

Note: I did a follow-up article about this, to cover HP switches as well.  The details of the Cisco data are below, though.

DHCP Option 82, Cisco switches and routers


When using dynamic DHCP to unknown clients, you generally need a way to track the switch ports that hosts attach to so they can be located in the event of misbehaviour.  I think the long term way to do this is something similar to what we've done with our wireless system: using MAC-based authentication to trigger RADIUS accounting messages (or periodic MAC address table scraping).  However, for the short term, I was wondering if I could get DHCP Option 82 to help me out.

DHCP Option 82 is intended for things such as metro Ethernet or cable modems, whereby a switch providing an edge port will insert it into a DHCPREQUEST packet to indicate the switch and port where it was received, thus allowing the location of the host to be logged by the DHCP server.  It all looks promising but I've never got it to work - however, a bit of work today sorted that out.

The problem I had previously was that when Option 82 information is inserted into the DHCP packet, the DHCP server never saw the DHCPREQUEST.  This turned out to be the Cisco router: the default situation for packets received with Option 82 present, but the GIADDR (Gateway IP Address) field blank (0.0.0.0), is that the request will be discarded.  The GIADDR field is set by the relay agent (typically the router) to its address on the interface where the request was received, allowing the server to know which network the client is connected to.

In our situation, the edge switch will set Option 82 and leave GIADDR blank.  The router will relay the packet and fill in GIADDR.  I would imagine this is how most people would use it and it's odd that Cisco's default configuration doesn't do this.

Anyway, to fix this problem, the router interface must be set to trust the Option 82 information, even when GIADDR is blank, you do:

interface Vlan3008
 ip dhcp relay information trusted
 ip helper-address 172.28.208.86
 ip helper-address 172.28.208.87
 ...

... or for all VLANs, you can used the global context configuration command:

ip dhcp relay information trust-all

... then you can configure your edge switch with DHCP Snooping (and ARP Inspection, if required).

Which brings me on to part two of this - how to parse the Cisco Option 82 information in the ISC DHCP server and log it.

Cisco formats for Option 82 data


The edge switch command ip dhcp snooping information option format remote-id hostname above sets Option 82 to log the switch hostname rather than MAC address as the remote-id - I think that's more useful.

Cisco switches default to a binary-encoded vlan-mod-port for the circuit-id, although this can be overridden on a per-interface basis with something like:

interface GigabitEthernet1/0/2
 ip dhcp snooping vlan 3008 information option format-type circuit-id string Gi1/0/2:3008

... the second format is much nicer as you can just print it, but you have to manually set it on a per-port and per-VLAN basis, which is very tedious.  I don't like tedious things, so parsing the vlan-mod-port format is desirable.  There is a document called DHCP Option 82 Configurable Circuit ID and Remote ID on Cisco's website, but that just explains the ASCII variants, so I thought I'd document the default (binary) types too.

Remote ID


There are two formats for this, depending on whether it's the default (switch MAC address) or specified hostname:

012-
06 (= Length)Switch MAC address
1LengthHostname

Circuit ID


This also has two formats, depending on whether the vlan-mod-port option is used, or something custom:

0123456-
04 (= Length)VLAN ID (big endian)Module (slot)Port ID
1LengthPort and VLAN circuit-id string

Parsing Option 82 from Cisco devices in the ISC DHCP server


The following code, stuck at the top level in the dhcpd.conf file will log messages such as dhcpd: agent information 172.30.140.4 to 24:de:c6:c6:51:92 on sw-ucs-rnb-s4 port 1/2 VLAN 3008, if the agent information is present and the remote ID is in ASCII hostname format:

if ((exists agent.remote-id) and
   (substring(option agent.remote-id, 0, 1) = 1)) {

  log(
    info,
    concat(
      "agent information ",
      binary-to-ascii(10, 8, ".", leased-address),
      " to ",
      binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)),
      " on ",
      substring(option agent.remote-id, 2, extract-int(substring(option agent.remote-id, 1, 1), 8)),
      " port ",
      binary-to-ascii(10, 8, "", substring(option agent.circuit-id, 4, 1)),
      "/",
      binary-to-ascii(10, 8, "", substring(option agent.circuit-id, 5, 1)),
      " VLAN ",
      binary-to-ascii(10, 16, "", substring(option agent.circuit-id, 2, 2))));
}

The ISC server's expression syntax is fairly limited and makes coping with alternatives very difficult - for example, to handle the same thing as above but with the remote ID being a MAC address, it would require an appropriate complete copy of the statement above, in addition, if both are to be handled.