Saturday, July 5, 2014

IPv6 First Hop Security

IPv6 First Hop Security is a new topic for CCIE v5. It's important to note that at the time of this writing (June/July 2014), IPv6 FH Security is not supported in IOL, so this cannot be on the CLI-based parts of the lab yet, but it can be in diagnostics or the written.

The biggest barrier to understanding IPv6 FH Security is understanding the whole first hop process to begin with.  IPv6 changes this dramatically from the IPv4 model. First, we will examine IPv6 First Hop and determine where the security problems are.

First, let's answer a simple question: How do I receive a routable IPv6 address?

ICMP communicates nearly everything regarding IPv6 addressing. It's used for finding a router, building an address (typically), making sure it's unique, finding a DHCP server if necessary, locating other hosts, assigning a default route, etc.

That said, IPv6 has a major chicken-or-the-egg problem. You can't run ICMP effectively without having an IPv6 address already, but how can you have an address before... you have an address?

Enter link-local addressing. Link local addresses all exist within FE80::/10. Typically, and true on Cisco devices, this address is built as FE80::<address based on MAC address>. It can alternatively (as is done in modern Windows OS) be built from FE80::<random address>. The specific format of the address isn't necessary for my explanation, so I will consider that out-of-scope for this document, and it can be found in hundreds of other places.

Since my blog is geared towards the CCIE, and we know we'll be using all Cisco kit, we can assume that the MAC address method is what we'll be using.

So we've built our interface a "link local" address of FE80::FA66:F2FF:FEDE:FF1. This link-local is non-routable, and can only be reached on the local segment. We must immediately run a process called DAD (Duplicate Address Detection) to ensure that we're the only person using this address. When we built the link local address, we also joined a multicast group called a "solicted node multicast", which is a multicast address that's unique for our host. DAD sends a multicast to this solicited node, and if anyone else responds (they shouldn't, if the address is unique), then we drop the address and don't use it.

Assuming the address is unique, our next goal is to come up with a routable address in addition to our link local. Now that we have a link-local address, we can send out a Router Solicitation (RS) and ask for the global prefix we should be on.  Let's say our router's IPv6 address is 2001:100::1/64. The router will fire back a Router Advertisement (RA) to our host that it's prefix is 2001:100::/64. We'll use the same process we used for link-local earlier to assign our globally routable address. Using the same example above, that address would be 2001:100::FA66:F2FF:FEDE:FF1.

This process is called StateLess Address AutoConfiguration, or SLAAC. It's important to note that the router must send out an RA with a /64 length, or this process doesn't work.

Perhaps more important to our examples, the RA we got the prefix from also hands out the router's IPv6 link local address, which we can optionally use as our default gateway. Just to point out again: this is not a global address. SLAAC's default routing is always done with link-local addresses.

Let's look at the rather minor config for this thus far:

R1 ("router"):
ipv6 unicast-routing

interface Gigabit0/1
  ipv6 address 2001:100::1/64

R2 ("host"):
ipv6 unicast-routing ! May or may not be necessary for a "host", platform-dependent.

interface Gigabit0/1
  ipv6 address autoconfig default

Omitting the "default" above would prevent it from installing R1 as a default route.

R2(config-if)#do sh ipv6 int br | s GigabitEthernet0/1
GigabitEthernet0/1         [up/up]

R2#sh ipv6 route ::/0
Routing entry for ::/0
  Known via "static", distance 2, metric 0
  Route count is 1/1, share count 0
  Routing paths:
    FE80::F2F7:55FF:FE8D:96A2, GigabitEthernet0/1
      Last updated 00:00:10 ago

FE80::F2F7:55FF:FE8D:96A2 is R1's link local address.

Cisco routers all announce themselves as viable gateways (imagine that), so if you have a "host" router - perhaps a voice gateway or whatnot - you need to tell it not to send router advertisements:

R2(config)#int gig0/1
R2(config-if)#ipv6 nd ra suppress all

The suppress keyword indicates not to send periodic RAs, suppress all means don't respond to RSes.

An optional feature that's not on all platforms:
R2(config-if)#ipv6 address autoconfig prefix

That will make the router insert routes for any other routes on the same segment. So if your neighbor had a second IPv6 address of 13::1, we'd insert a route to it as well.

So that gets us a link-local address, global unicast address, and default gateway. What about DNS?

Up until very recently, the only real option was to run a stateless DHCPv6 server. Stateless because in this scenario, the DHCPv6 server doesn't actually keep track of anything, it just hands out options: DNS, Call Manager info, etc.

R1(config)#ipv6 dhcp pool DHCP-POOL
R1(config-dhcpv6)#dns-server 4::4
R1(config-dhcpv6)#dns-server 8::8
R1(config-dhcpv6)#domain-name ABC.COM
R1(config-dhcpv6)#int gig0/1
R1(config-if)#ipv6 dhcp server DHCP-POOL
R1(config-if)#ipv6 nd other-config-flag

The process on R2 is automatic, but on R1 we create the pool, which is reasonably obvious config, apply it to the interface, and then set the O-flag. This tells clients via RA that it should query for a DHCP server for more information. The DHCP server and the device sending the RA do not need to be the same device.

Great! We've got our addresses, default gateway, and DNS. Before we move on to neighbor discovery, let's look at stateful DHCP.

You've got a couple options here.

Either way we need more info in our DHCP server:

R1(config)#ipv6 dhcp pool DHCP-POOL
R1(config-dhcpv6)#address prefix 2001:100::/64

We'll still get our default gateway through RAs, but we can at least track the addresses that our hosts are using.

Our first option is to just recommend to our host that it use stateful DHCP.

R1(config-if)#no ipv6 nd other-config-flag
R1(config-if)#ipv6 nd managed-config-flag

The M-Flag (Managed Flag) is a suggestion to the client that it should use the DHCP server for its host address instead of SLAAC.

Since we're using a Cisco client, I need to stop here and advise that I've never been able to get the client to recognize the M-flag. It could be my IOS, I haven't investigated it that much.

Since that's a bust, let's look at the other method.

R2(config-if)#int gig0/1
R2(config-if)#ipv6 address autoconfig default-route ! still need this for the default gateway
R2(config-if)#ipv6 address dhcp
R2(config-if)#ipv6 enable

I'll explain ipv6 enable momentarily.

R2(config-if)#do sh ipv6 int br | s GigabitEthernet0/1
GigabitEthernet0/1         [up/up]

We got our address. (Side-note: if you have ipv6 address dhcp on an interface and not ipv6 address autoconfig default-route, you won't get a connected route to the local subnet - you get a totally unusable /128 host address and that's it. The workaround is to disable ipv6 unicast-routing, then you'll get the connected route)

R1#sh ipv6 dhcp binding
Client: FE80::FA66:F2FF:FEDE:FF1
  DUID: 00030001F866F2DE0FF0
  Username : unassigned
  IA NA: IA ID 0x00040001, T1 43200, T2 69120
    Address: 2001:100::2C11:9212:8690:D8FE
            preferred lifetime 86400, valid lifetime 172800
            expires at Jul 02 2014 01:29 AM (172654 seconds)

and R1 knows about us - it's stateful.

There's also a new option defined by RFC 6106 that completely eliminates the need for DHCPv6 in relation to DNS. Unfortunately, at the time of this writing, you need bleeding-edge IOS-XE in order to use it. In fact, the hardware lab I'm using (which will be explained later) for this doesn't even support it - I have to turn to CSR1000v:

Router(config-if)#ipv6 nd ra dns server ?
  X:X:X:X::X  IPv6 address

This passes DNS servers as an RA option. I have labbed this previously, it does appear to work from what I can see in Wireshark.

I promised an explanation to ipv6 enable. I bet you've seen this command before, followed by a statically configured address. You actually need this very infrequently. With the exception of the DHCP example I showed above, the only time you need ipv6 enable is if you want a eui-64 derived link-local address without a global unicast address. If you're statically assigning an IPv6 global unicast, or you're getting one via SLAAC, you don't need this command - so cut it out! :)

Of course your final option is a statically assigned address:

ipv6 address 1::1/64 

Link locals can also be statically assigned:

ipv6 address FE80::1 link-local

Now that we're past "how do we get an address", let's move on to the other big topic of "how do we find our neighbors". As you're probably already aware, IPv6 does not use ARP, instead it uses a Neighbor Solicitation (NS) and Neighbor Advertisement (NA) ICMPv6 messages. This builds the neighbor table instead of the ARP cache.

NS and NA map pretty directly in functionality to ARP request and ARP reply in IPv4. I'm not going to go over them in detail, just understanding their function is sufficient for now.

If R2 has just gotten it's address and wants to find R3, it would send out an NS.
I've reset R2 to SLAAC and setup R3 for SLAAC as well.
R2: 2001:100::FA66:F2FF:FEDE:FF1
R3: 2001:100::C67D:4FFF:FEF9:5340

R2#ping 2001:100::C67D:4FFF:FEF9:5340

Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:100::C67D:4FFF:FEF9:5340, timeout is 2 seconds:
Success rate is 100 percent (5/5), round-trip min/avg/max = 0/3/16 ms

and some filtered output from debug ipv6 icmp:

ICMPv6: Sent N-Solicit, Src=2001:100::FA66:F2FF:FEDE:FF1, Dst=FF02::1:FFF9:5340
ICMPv6: Received N-Advert, Src=2001:100::C67D:4FFF:FEF9:5340, Dst=2001:100::FA66:F2FF:FEDE:FF1

There's our NS going out, and NA coming in. It's important to note that the NS also provided R3 with R2's layer 2 address, so there's no need for the reverse process to happen.

R2#sh ipv6 neighbor
IPv6 Address                              Age Link-layer Addr State Interface
2001:100::C67D:4FFF:FEF9:5340               0 c47d.4ff9.5340  REACH Gi0/1

R3#sh ipv6 neighbor
IPv6 Address                              Age Link-layer Addr State Interface
2001:100::FA66:F2FF:FEDE:FF1                0 f866.f2de.0ff1  REACH Gi0/0

As you can see, R2 is aware of R3's addresses and vice-versa.

Before I move on to pointing out the (somewhat obvious) security problems with this whole process, I'd like to pause and look at a few other features that I felt were of important note, but didn't fit into the explanation thus far.

If you have multiple routers on a segment and want to use one or more for failover, you can specify how important their advertisements are:

ipv6 nd router-preference med !default (setting this doesn't show in config)
ipv6 nd router-preference low !depreffed
ipv6 nd router-preference high !preffed

Although, I imagine most of us would just use HSRP.

Also, if you set the RA lifetime to 0, hosts won't use it as a default gateway, but it can still be used for SLAAC: ipv6 nd ra lifetime 0

I've previously run production dual-stack IPv6 at home. It was an interesting experiment, but the overwhelming lesson I learned from it is that most content providers are using separate pipes for their IPv6 traffic, and instead of being wide open and unused liked I'd hoped, they were miserably oversubscribed, so when my Windows installation naturally preferred a IPv6 DNS resolution when both IPv6 and IPv4 were available for the same site (Here's looking at you, Youtube!) all I got was slower web traffic. I still have the v6 service on my cablemodem, but I disabled it on the router.

One of the more interesting things I learned from it was the sudden realization of what the heck I do without NAT.  Almost all home networks are setup like so on IPv4:

[provider] ---- outside /30 ----> [home router] (NAT) --- private IP range ---> hosts

So if you think about this for a minute... in order to duplicate this on IPv6, which doesn't typically use NAT, you need a routable IPv6 address block on the outside of your home router, and a routable IPv6 address block on the inside of your router too. And since home ISPs don't exactly assign you two blocks of static addresses typically, you're also going to need a way to do this with DHCP or SLAAC.

There's a really clever fix for this in IPv6. It's called prefix delegation. The idea is that our provider will delegate an IPv6 block to our router, which will then in turn function as the DHCP server for that block.

Let's look at some sample config.

ISP router:
ipv6 local pool dhcpv6-pool1 2001:DB8:1200::/40 48

ipv6 dhcp pool test
 prefix-delegation pool dhcpv6-pool1 lifetime 1800 600
 dns-server 4::4
 dns-server 8::8 

interface GigabitEthernet1/0
 ipv6 address 12::1/64
 ipv6 nd other-config-flag
 ipv6 dhcp server test 

Here we've told the ISP router that it should delegate /48 blocks from a larger /40 block to every DHCP server that asks. 

Home router:
ipv6 dhcp pool MY-DNS  ! we'll use our DNS servers
 dns-server 44::44
 dns-server 88::88

interface GigabitEthernet1/0
 ipv6 address autoconfig default  ! we want a SLAAC address
 ipv6 dhcp client pd FOO  ! we'll collect prefix delegation for the next interface

interface GigabitEthernet2/0
 ipv6 address FOO ::/64 eui-64  ! we'll use a /64 out of whatever we got above
 ipv6 nd other-config-flag
 ipv6 dhcp server MY-DNS

So, lots going on on the home router.  First, we're going to assign our outside interface - Gig1/0 - an address via SLAAC.  This gives us the IPv6 equivalent of my "outside /30"  from my ASCII diagram above, albeit it on a much larger block than a IPv4 /30 :).  Next, we're going to create a prefix delegation named FOO, and pick up a prefix from our ISP server, which we already know will be a /48 from my explanation above. We'll then go to our inside interface, Gig2/0, and assign ourselves an EUI-64 address from the prefix delegation FOO. We'll of course be sending RAs, so our internal hosts will also use SLAAC to get an address on the inside, delegated subnet.

Our end device/host would have a simple config, just running SLAAC and being a stateless DHCP client. In IOS it'd just be:

interface GigabitEthernet1/0
 ipv6 address autoconfig default

Back on the ISP server, you get reverse route injection in the form of static IPv6 routes pointing to whomever you delegated the prefix to. These can be redistributed into your IGP or BGP.

Now let's look at what security faults all these features potentially have.

There's some obvious "for like" issues that IPv4 had, that carried over to IPv6:

- An attacker could pose as the stateful DHCP server, handing out either bad information for denial of service (DOS), or handing out an attacker's IPv6 address for a man-in-the-middle attack (MiM).
- An attacker could pose as another host. Let's say Host1 is trying to reach Host2, and Host3 is an attacker.  Host1 could send out a NS for Host2, and Host3 could send an NA masquerading as Host2, and Host1 may accept it as true if timed correctly. A similar process could be done from Host2 -> Host1, with Host3 - inserting itself there as well, and effectively inserting itself into the entire conversation - a MiM attack - with Host1 and Host2 none the wiser.

This is where the similarities with IPv4 end. Here are some of the new security concerns:

- A router advertisement could be faked, allowing a host to insert itself into a conversation, for a MiM attack.
- A router could advertise, from one router to another, a prefix that shouldn't be on the local link. This connected route would be seen as closer than a theoretical downstream router that's legitimately advertising the same prefix.
- An attacker could respond to every DAD request from a host, or even from the entire segment, effectively preventing hosts from using their legitimately unique IPv6 addresses, creating a DOS.
- An outside host - not one on your local segment - could send traffic in rapid-fire towards a large swath of your available IPv6 space. Many IPv4 segments were either behind NAT or simply weren't all that big, but IPv6 space is really, really large - a /64 - the common LAN segment size - is 18 quintillion addresses. No last-hop router has enough memory to send NS requests for 18 quintillion addresses without running out of memory or CPU, creating a DOS by worst-case crashing the router, best-case busying the CPU out to the point where new requests aren't serviced.

For me, the most confusing thing about IPv6 FH Security was that there are, as I understand it, an "old" way of doing things, and a "new" way of doing things. These overlap a lot, and there's no real discussion of this in the documentation, so figuring out when to use what was confusing.

We're going to start with the "old" way, which is reasonably well-documented and well-blogged on the Internet, but not particularly thorough, and then move on to the "new" way, which seems more complete.

First let's see about tackling invalid RAs.

The simplest, non-automatic way to prevent invalid RAs is to write an PACL for them:

ipv6 access-list IPv6
 deny icmp any any routeradvertisement
 permit ipv6 any any

and simply apply it to any device that shouldn't be sending RAs.

There's a gotcha here, in that there is a well-known exploit by sending fragmented packets. I'm not going to go into this in detail as hundreds have blogged about this already. The workaround is to add one more deny to the PACL:

deny ipv6 any any undeterminedtransport

This will make the ACL drop any IPv6 traffic where the router is unable to determine the transport type (no layer 4 information). Some say this makes for better security than the actual RA Guard feature, but debates like that are out of scope for the CCIE, so I'll leave that to others.

This feature is simple and well-documented enough that I'm not going to lab it.

Moving on to features we will be labbing, let's look at RA Guard. It's the automated, more granular version of what we just did with the PACL.

Here's the diagram we'll work off of for the remainder of the article.

As I have in many other blogs, I use GNS3 for diagramming, but I'm using physical gear for the lab. Unfortunately, IPv6 FH Security requires a bleeding-edge IOS or IOS-XE layer 2 device, so labbing it is not so easy. I was lucky enough that some friends were able to lend me a 4948-E running 15.2(1). So, I am running entirely physical gear in this lab, despite what's implied by the diagram above.

This is also good and bad, because I'm using a remote lab, I can't recable on the fly. So there are a few scenarios that I didn't lab out as thoroughly as I would've done normally, but the knowledge gained here should still be more than sufficient for what may appear on the lab.

R1 will represent our valid router.
R2 will (usually) represent our valid host.
R3 will represent our attacker.

The most common way to set this up is to basically un-trust all ports that shouldn't have a router on them, and trust all the ones that should. This is accomplished with this config:

ipv6 nd raguard policy MY_ROUTERS
 device-role router

ipv6 nd raguard policy MY_HOSTS
 device-role host  ! DEFAULT

ipv6 snooping logging packet drop  ! NEEDED FOR LOGGING

vlan configuration 123 ! all our hosts are on vlan 123
  ipv6 nd raguard attach-policy MY_HOSTS

int gig1/1
  ipv6 nd raguard attach-policy MY_ROUTERS

I know the first thing I thought when I saw this was "what the heck is 'vlan configuration XYZ'"?
I always did find it a bit strange back on the Catalyst 3560, when I learned it for CCIE v4, that QoS config would be put on an SVI even though the SVI sometimes had no IP address on it - you'd just create it for the QoS config. I guess someone at Cisco thought the same way; so now there's a specific configuration section for VLANs.

So in short what we accomplished above was to configure every port in vlan 123 to assume a "host" was attached to it - a host should never be sending RAs. We then overrode that configuration on gig1/1 and told the switch to expect a router there - routers are, of course, OK to hear RAs from. Interface configuration always overrides vlan configuration, and that's true for the rest of the features we'll review in this article, so I'm going to assume that's understood from here on in.

We enabled logging via ipv6 snooping logging packet drop, which will actually turn on the majority of the logging we need for this article. There is one additional command we'll cover later.

I instructed R2 and R3 not to send RAs, let's see what output we get if I attempt to enable them on R3.

R3-ATTACKER(config-if)#ipv6 nd router-preference high ! Let's attempt to become the preferred router
R3-ATTACKER(config-if)#no ipv6 nd ra suppress all

*Jul  3 18:15:58.734: %SISF-4-PAK_DROP: Message dropped A=FE80::C67D:4FFF:FEF9:5340 G=- V=123 I=Gi1/3 P=NDP::RA Reason=Message unauthorized on port

That's the extreme basics, and I did it in long-hand so to speak, here's the shorter way to accomplish the same thing with defaults:

ipv6 nd raguard policy MY_ROUTERS
 device-role router

vlan configuration 123
  ipv6 nd raguard

int gig1/1
  ipv6 nd raguard attach-policy MY_ROUTERS

This is true of just about every filtering command for IPv6 FH Security, if you use just the basic command with no policy, i.e. ipv6 nd raguard, you get the default untrusted configuration. Here, we just said don't trust any ports' RAs except Gig1/1.

Filtering can also be done per-VLAN: 

interface GigabitEthernet1/1
 switchport trunk allowed vlan 122,123
 switchport mode trunk
 ipv6 nd raguard attach-policy MY_ROUTERS vlan 123
 ipv6 nd raguard vlan 122

This hypothetical config would trust RAs on vlan 123, but not vlan 122.

There are many other options for RA Guard:

ipv6 nd raguard policy SAMPLE
 device-role router
On the 4948, I have four options here: router, host, switch, and monitor.
Router and Host we already covered. Switch, I don't understand, and I can't find any documentation explaining what it does. Suffice to say that out-of-the-box it doesn't allow RAs, so if you set it with no other options, you get something similar to "Host". Monitor I found some vague definitions on, but it too has a similar outcome as switch: No RAs.
 other-config-flag on
 Require the other-config-flat to be set in the RA or the policy will not pass the traffic.
Permits RAs -- I can't find any other benefits to it for RA Guard, although the other frameworks (name ND Inspection) preference ports that have trusted-port enabled if they have a conflict.
 router-preference maximum
Sets the highest allowed preference on the port. If you want to enforce a backup router to being "low" or "medium" preference, this will drop the packet if anything higher is advertised.
 hop-limit minimum
 hop-limit maximum 
RAs advertise what hosts should use as a TTL. You can use these two functions to control what the minimum and maximum advertised TTLs should be. Note, if you want to test this with all IOS devices, IOS can only send one of two options for this field - the default, which is 64 (this is NOT noted on the CLI anywhere, I figured it out the hard way) - or "unspecified", which basically means "use whatever you want".
If you want to match a specific size, such as 90, you would set minimum and maximum to the same value.
 managed-config-flag on ! Ensures the managed config flag ("please get your IPv6 address statefully from DHCPv6") is on, if not, drop the packet.
 match ipv6 access-list <ACL>
This will match the link local address that's advertising the RA. If no match on the access-list, drop the packet. Formatting is as such: permit ipv6 host FE80::F2F7:55FF:FE8D:96A1 any
 match ra prefix-list <prefix list>
This will match the prefix being advertised in the RA. If no match, drop the packet.

Covering each of these options would make the article drag on and on, so I'm going to give one large configuration and comment on it:

ipv6 access-list LL-EXAMPLE
  Permit ipv6 host FE80::F2F7:55FF:FE8D:96A1 any

ipv6 prefix-list PREFIX-EXAMPLE permit 2001:100::/64

ipv6 nd raguard policy SAMPLE
 device-role router
 other-config-flag on
 router-preference maximum medium
 hop-limit minimum 64
 hop-limit maximum 64
 match ipv6 access-list LL-EXAMPLE
 match ra prefix-list PREFIX-EXAMPLE

int gig1/1
  ipv6 nd raguard attach-policy SAMPLE

This sample would allow RAs, require the other-config-flag to be enabled, not allow a router preference higher than medium, ensure a strict TTL advertisement of 64, require the RA to be sourced from FE80::F2F7:55FF:FE8D:96A1, and only permit it to advertise 2001:100::/64.

DHCP Guard is a tad simpler.

We'll setup R1 as a stateful DHCP server, R2 as a DHCP client, and R3 as a malicious stateful DHCP server. I've removed all the RA Guard configuration to decrease the example complexity.

R1-ROUTER(config)#ipv6 dhcp pool TEST-POOL
R1-ROUTER(config-dhcpv6)#address prefix 2001:100::/64
R1-ROUTER(config-dhcpv6)#dns-server 4::4
R1-ROUTER(config-dhcpv6)#dns-server 8::8
R1-ROUTER(config-dhcpv6)#int gig0/0
R1-ROUTER(config-if)#ipv6 dhcp server TEST-POOL

R3-ATTACKER(config-if)#ipv6 dhcp pool TEST-POOL
R3-ATTACKER(config-dhcpv6)#address prefix 2001:101::/64
R3-ATTACKER(config-dhcpv6)#dns-server 2001:101::BAD
R3-ATTACKER(config-dhcpv6)#int gig0/0
R3-ATTACKER(config-if)#ipv6 address 2001:101::BAD/64
R3-ATTACKER(config-if)#ipv6 dhcp server TEST-POOL

and on our switch:

ipv6 prefix-list GOOD-PREFIX seq 5 permit 2001:100::/64 le 128 
ipv6 access-list LL
 permit ipv6 host FE80::F2F7:55FF:FE8D:96A2 any

ipv6 dhcp guard policy TRUSTED
 device-role server
 match server access-list LL
 match reply prefix-list GOOD-PREFIX

vlan configuration 123
 ipv6 dhcp guard attach-policy TRUSTED

I'm going to treat this a bit differently - I'm going to trust all ports, but require them to match a specific link local source and address range.

R2-HOST(config-if)#no ipv6 address autoconfig
R2-HOST(config-if)#ipv6 enable
R2-HOST(config-if)#ipv6 address dhcp

*Jul  3 20:39:34.985: %SISF-4-PAK_DROP: Message dropped A=FE80::C67D:4FFF:FEF9:5340 G=2001:101::68:6AF1:6FC2:5983 V=123 I=Gi1/3 P=DHCPv6::ADV Reason=The source address in the DHCPv6 ADVERTISE packet is not authorized by the DHCP Guard policy

R2-HOST#sh ipv6 int br | s GigabitEthernet0/0
GigabitEthernet0/0         [up/up]

We get the correct address from the correct server.

Now moving on to a-la-carte Neighbor Discovery Inspection.

The first thing to understand about Neighbor Discovery is that it's a control plane feature only - it doesn't inspect actual data traffic, it is only looking at ND ICMP packets, and that's it - so if your users spoof their source address in an actual traffic flow, this doesn't protect against it.

IPv6 ND Inspection builds a table based on NS/NA messages. It then enforces the table. The funny thing about it is, with ND/NA, basically the first one it hears becomes the trusted one.

WS-C4948E(config)#vlan configuration 123
WS-C4948E(config-vlan-config)#ipv6 nd inspection

I really expected DHCP Guard to also populate this table, but the a-la-carte version of ND Inspection and DHCP Guard don't appear to talk to one another, as best I was able to tell. The unified (ipv6 snooping) feature, which we will see next, does populate the neighbor bindings from DHCP, so don't be confused as to why you see DHCP as a population feature in the screen shot.

So let's spoof R2 (Gig1/2)'s link local from R3:

R3-ATTACKER(config-if)#ipv6 nd dad attempts 0  ! DAD would prevent the spoof
R3-ATTACKER(config-if)#ipv6 address FE80::FA66:F2FF:FEDE:FF1 link-local

*Jul  4 14:45:01.109: %SISF-4-PAK_DROP: Message dropped A=FE80::FA66:F2FF:FEDE:FF1 G=- V=123 I=Gi1/3 P=NDP::RA Reason=More trusted entry exists

More trusted entry exists.  As you'll see above, there is a "Preflevel" numbering system, which I think of as administrative distance for neighbor bindings. The higher the number, the more trusted the entry is. Although I do find the message odd when you try to spoof -- what it really should say is "I already learned this entry from someone else, first come first serve!"

The most trusted method is a static entry, which looks like:

ipv6 neighbor binding vlan 123 FE80::FA66:F2FF:FEDE:FF1 interface gig1/3 c47d.4ff9.5340

You can shorten this up a whole bunch, by omitting the optional VLAN and MAC address, but in my case, I'm trying to override the legitimate link-local of R2 to allow R3 to ping from its link local. Without "fully populating" the table, it still prefs the dynamically learned entry over the static one.

We see the static entry with the priority of 100 - let's see if we can ping from R3 now, with our spoofed link-local:

R3-ATTACKER#ping FE80::F2F7:55FF:FE8D:96A2 source FE80::FA66:F2FF:FEDE:FF1
Output Interface: GigabitEthernet0/0
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to FE80::F2F7:55FF:FE8D:96A2, timeout is 2 seconds:
Packet sent with a source address of FE80::FA66:F2FF:FEDE:FF1%GigabitEthernet0/0
Success rate is 100 percent (5/5), round-trip min/avg/max = 0/0/0 ms

Let's look at the rest of the features you can optionally put in a policy.

ipv6 nd inspection policy TEST-POLICY
 limit address-count X
This is relatively obvious, it limits how many addresses are permitted to participate in the ND process on a port. Keep in mind you always need a minimum of 2 if you have a global unicast: you need the link-local as well.
 tracking enable
This is liveliness tracking, and is pretty cool. We'll give this it's own paragraph below.
There's a cryptographic version of neighbor discovery called SeND. It requires a PKI infrastructure and certificates all the way down to the client. I'm going to call SeND "out of scope" for the CCIE v5 based on complexity. drop-unsecure is regarding SeND, so I'm skipping it.
 sec-level minimum 
Also used for SeND; out of scope.
 device-role {host | monitor | router} 
I'm not really sure what the point is here - I don't know how host or router would differ, and Cisco hasn't documented it.
 validate source-mac
I imagine this validates that the source MAC is appropriate for future ND communication, but it's not documented and I can't lab it without changing wires (my lab is remote, as mentioned above), so unfortunately I haven't got a good explanation on this option.

If you enable tracking, as shown above, hosts get probed with an "are you still there?" if no ND packets are heard periodically.

WS-C4948E(config)#ipv6 nd inspection policy ND-TEST
WS-C4948E(config-nd-inspection)#tracking enable
WS-C4948E(config)#vlan configuration 123
WS-C4948E(config-vlan-config)#ipv6 nd inspection attach-policy ND-TEST

Note the "time left" field on the far right. This is how long until a NS is sent out to the neighbor asking if it's still there. This is useful for two reasons:
- Since the table is first-come first-serve, this frees up address space from being held indefinitely if it's actually not in use.
- It allows for a host to move - imagine unplugging from one switchport and moving to another on the same switch - we need a way to age out the information reasonably quickly.

We see we have 220 and 228 seconds on the two hosts presently in the table until they're probed.

If we have an IPv6 address on the appropriate VLAN, we'll send a NS sourced from our IPv6 address.
It sure confused me - if we have a pure layer 2 device - how will it send an NS?

If we have a link local on the network, we'll send a NS from our IPv6 address. If we don't have an IPv6 address on the VLAN (we're just switching L2 only), we'll send an NS from the IPv6 unspecified address: that's right, the switch will send NSes even if IPv6 routing isn't enabled.

If you want probes to go out more frequently than every 300 seconds, you can set it like so:

ipv6 nd inspection policy TEST-POLICY
 tracking enable reachable-lifetime 15 ! this will set it for 15 seconds.

The most confusing feature for me was the "ipv6 snooping" syntax, which accomplishes basically everything we saw above, plus has the option (via source guard and destination guard) to filter data plane traffic as well. The confusing part about it is simple: it has lots of cross-over with all the previous functions, yet it uses a different syntax.

The basic concept of ipv6 snooping is to build the neighbor database, similar to what IPv6 ND inspection did, except it uses and enforces more methods all at once.

It can use:
- Information from DHCP (Default)
- Information from ND (Default)
- Static bindings

I'm wiping out all prior FH Security functions implemented above, we're starting from scratch with basic addressing. Since this is a large topic, I'm going to give increasingly more complicated examples and explain them one at a time.

At it's basics, enabling it is very simple:

WS-C4948E(config)#vlan configuration 123
WS-C4948E(config-vlan-config)#ipv6 snooping

Unfortunately this totally breaks the network.

By default, ipv6 snooping enables its version of RA Guard and ND Inspection, so now RAs won't work any longer.

*Jul  5 11:41:42.007: %SISF-4-PAK_DROP: Message dropped A=FE80::F2F7:55FF:FE8D:96A2 G=- V=123 I=Gi1/1 P=NDP::RA Reason=Packet not authorized on port

So let's fix that.

WS-C4948E(config)#ipv6 snooping policy TRUST_ROUTER
WS-C4948E(config-ipv6-snooping)# security-level glean
WS-C4948E(config-ipv6-snooping)#int gig1/1
WS-C4948E(config-if)#ipv6 snooping attach-policy TRUST_ROUTER

R2-HOST(config-if)#ipv6 address autoconfig default
R2-HOST(config-if)#do sh ipv6 int br | s GigabitEthernet0/0
GigabitEthernet0/0         [up/up]

Ok great! Now what the heck does "security-level glean" mean?  Let me tell you, it was a lot of 'fun' to figure out from the near zero explanation the docs give. glean learns from DHCP and ND, but doesn't enforce anything. So "glean" basically means "trust this port". There is also a trusted-port option on the policy, and on my IOS, it appears to do absolutely nothing (how handy).

For purposes of keeping this article a reasonable size, I'm going to go ahead and point out the 1:1 features in common with the a-la-carte ND inspection. By default, when you enable snooping, you're getting integrated ND Inspection. If you don't want it, you would:

WS-C4948E(config)#ipv6 snooping policy MY_POLICY
WS-C4948E(config-ipv6-snooping)#no protocol ndp

In this case, it would only learn entries from DHCPv6 "Guard" and static entries. Of note, you can do the same thing to disable dhcp inspection: no protocol dhcp

Assuming you left protocol ndp on, these features work the same way I explained them above:

limit address-count
  Make sure you allow at least 2 if you're using global unicast.
  Same as before, send NSes to keep the table up to date.

So let's look at the default integrated DHCP Guard inspection yet. As mentioned above, this is on by default when using IPv6 Snooping.

R1-ROUTER(config)#ipv6 dhcp pool DHCP-POOL
R1-ROUTER(config-dhcpv6)#address prefix 2001:100::/64
R1-ROUTER(config-dhcpv6)#dns-server 4::4
R1-ROUTER(config-dhcpv6)#dns-server 8::8
R1-ROUTER(config-dhcpv6)#int gig0/0
R1-ROUTER(config-if)#ipv6 dhcp server DHCP-POOL

Now I've already pre-trusted this port (with the glean security level mentioned above), so it's also OK to be a DHCP server.

R2-HOST(config)#int gig0/0
R2-HOST(config-if)#ipv6 enable
R2-HOST(config-if)#no ipv6 address autoconfig default
R2-HOST(config-if)#ipv6 address dhcp
R2-HOST(config-if)#do sh ipv6 int br | s GigabitEthernet0/0
GigabitEthernet0/0         [up/up]

We got our DHCP address.

And we now learned a binding via "DH" - DHCP.

There are three security models that can be applied to entire VLANs or per-port/per-vlan:

WS-C4948E(config-ipv6-snooping)#security-level ?
  glean    Glean addresses
  guard    inspect and drop un-authorized messages (default)
  inspect  glean and Validate message

We've discussed glean - it basically trusts the port but still keeps track of the bindings. Guard, as indicated above, is the default, and it's what we're getting when nothing is specified. Guard is, in effect, the same as enabling the a-la-cart DHCP Guard, RA Guard, and ND Inspection: It learns bindings and denies untrusted (non-glean) ports from sending RAs, DHCP offers, invalid DAD, invalid NAs, etc. It's an all-in-one control-plane security enforcer! 

Best I can tell, "inspect" enforces ND only (similar to the a-la-carte ipv6 nd inspection feature) but doesn't protect against malicious RAs or DHCP servers. I validated this by enabling it on all interfaces, sending RAs and DHCP off R1, and then attempting to spoof R2's address on R3's interface. Everything was permitted except the R3 spoof of R2, which produced:

*Jul  5 13:38:48.559: %SISF-4-PAK_DROP: Message dropped A=2001:100::9193:876E:7E83:F33C G=- V=123 I=Gi1/3 P=NDP::NA Reason=More trusted entry exists

on the 4948.

That wraps up the control-plane filters, data-plane filters are next, including Source Guard, Destination Guard, and Prefix Guard. IPv6 snooping builds the database that these features use, so it is a prerequisite for everything we'll see from here on.

Source guard is very simple. If the data-plane traffic - any traffic other than IPv6 ND/RA and DHCP - doesn't match the source address present in the prebuilt binding table, drop the traffic.

WS-C4948E(config)#vlan configuration 123
WS-C4948E(config-vlan-config)#ipv6 source-guard

The main additional configuration for this is to validate prefixes, which is what's known as Prefix Guard.

Far earlier in the article we discussed prefix delegations via DHCP. This is the technology that allows you to sub-lease a prefix from one DHCP server to a downstream DHCP server. I couldn't get this feature to work with my lab, and I believe it's a platform limitation, but unfortunately when you're borrowing high-end switches to lab bleeding-edge functions, beggars can't be choosers.

Here is how I think it's supposed to work:

ipv6 local pool dhcpv6-pool1 2001:100:123::/40 48

ipv6 dhcp pool test
 prefix-delegation pool dhcpv6-pool1 lifetime 1800 600
 dns-server 4::4
 dns-server 8::8 

interface GigabitEthernet0/0
 ipv6 address 2001:100:123::1/48
 ipv6 nd other-config-flag
 ipv6 dhcp server test 

interface GigabitEthernet0/0
 ipv6 address autoconfig default
 ipv6 nd ra suppress all
 ipv6 dhcp client pd PREFIX-DELEGATION

interface Loopback0  ! I needed something to pretend to be a downstream client
 ipv6 address PREFIX-DELEGATION ::/64 eui-64  
 ipv6 enable

ipv6 snooping policy SNOOPING-POLICY
 security-level glean
 prefix-glean  ! this learns prefix delegations - this did work for me, it was the filtering that didn't work.

ipv6 source-guard policy PREFIX-GUARD
 no validate address
 validate prefix

vlan configuration 123
 ipv6 snooping attach-policy SNOOPING-POLICY
 ipv6 source-guard attach-policy PREFIX-GUARD

And after applying all this, my switch shot back at me:

WS-C4948E(config)#%warning% This filter is not supported. Vlan - 123,  mac - any, prefix_length - 64%warning% This filter is not supported. Vlan - 123,  mac - any, prefix_length - 64%warning% This filter is not supported. Vlan - 123,  mac - any, prefix_length - 128  

I tinkered with it for a while, changing the address to another one on Lo0 that I shouldn't be able to use, to no avail. Traffic kept passing.  So I'm assuming this just can't be accomplished on this hardware or I'm hitting a bug.

I did have one other curiosity with source guard (just vanilla source guard, not prefix guard); while it did filter traffic appropriately, sometimes when I disabled the feature, traffic still wouldn't forward. Clearing the database solved the problem: clear ipv6 neigh bind

Our last major topic is Destination Guard, which is pretty darn cool. The recommended prefix size for a LAN segment in IPv6 is a /64, which is 18 quintillion addresses. To put that into perspective, that's 18,446,744,073,709,551,616 addresses in one segment.

What would happen if you tried to "arp" (IPv6 NS) for 18,446,744,073,709,551,616 addresses back-to-back? 

Your router would melt. You'd run out of RAM and CPU very quickly. At a best case this would result in a simple DOS where legitimate NSes couldn't get through for a while, at a worst case the OS might crash. Most of the attacks we've looked at so far require the attacker to have access to the "inside" of your network, what's worse about this attack is that it can be accomplished from the outside of your network - potentially from the Internet.

Destination Guard addresses this. Destination Guard is a "last hop" security feature: the attack can be launched from anywhere on a routed network, and the last hop router is the only one that is heavily impacted, because interim routers don't have to NS for the final destination, they just CEF-switch the packet.

We're also assuming, for our purposes here, that the last-hop router is a layer 3 switch supporting destination guard.

WS-C4948E(config)#vlan configuration 123
WS-C4948E(config-vlan-config)#ipv6 destination-guard

There's only one very simple setting you can add if you use a policy:

WS-C4948E(config)#ipv6 destination-guard policy FOO
WS-C4948E(config-destguard)#enforcement ?
  always    Enforced under all conditions (default)
  stressed  Enforced when system is under stress

stressed isn't defined anywhere, but I'm assuming it means only kick this feature in during high CPU load, or perhaps under a great deal of NS.

I'll be changing our lab up a bit in order to test this, R1 will be out of the picture, R2 will have a static assignment and will be our "trusted" host, and R3 will be our attacker on the Internet.

R2-HOST(config-if)#int gig0/0
R2-HOST(config-if)#ipv6 address 2001:600D::2/64 ! my best representation of "GOOD" in hex :)
R2-HOST(config)#ipv6 route ::/0 2001:600D::1  ! the switch's address

R3-ATTACKER(config-if)#int gig0/0
R3-ATTACKER(config-if)#ipv6 address 2001:BAD::2/64
R3-ATTACKER(config)#ipv6 route ::/0 2001:BAD::1

WS-C4948E(config)#ipv6 unicast-routing
WS-C4948E(config)#vlan 200
WS-C4948E(config)#vlan 300
WS-C4948E(config)#int vlan200
WS-C4948E(config-if)#ipv6 address 2001:600D::1/64
WS-C4948E(config-if)#no shut
WS-C4948E(config-if)#int vlan 300
WS-C4948E(config-if)#ipv6 address 2001:BAD::1/64
WS-C4948E(config-if)#no shut
WS-C4948E(config)#int gig1/2
WS-C4948E(config-if)#switchport access vlan 200
WS-C4948E(config)#int gig1/3
WS-C4948E(config-if)#switchport access vlan 300

WS-C4948E(config-if)#vlan configuration 200,300
WS-C4948E(config-vlan-config)# ipv6 snooping
WS-C4948E(config-vlan-config)# ipv6 destination-guard

Now let's say the "BAD" neighbor, a host on the Internet, tries to hammer away at tens of thousands of addresses on the "GOOD" (600D) network in order to make the router (our 4948) collapse under the NS load.

In order to see this security feature take effect, we need to enable one more logging feature:

WS-C4948E(config)#ipv6 snooping logging resolution-veto

What the heck is resolution veto? If the switch decides it's getting asked to resolve for bogus addresses, it will "veto" the neighbor solicitation.

Before I try the attack, I went ahead and verified connectivity from R2 to R3. 

R2-HOST(config)#do ping 2001:BAD::2

Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:BAD::2, timeout is 2 seconds:

That should've worked. What did the 4948 have to say?

*Jul  5 16:13:12.774: %SISF-4-RESOLUTION_VETO: Resolution vetoed NS for D=2001:BAD::2 on I=Vl300 reason=Destination not active on link

This is an important point on this feature: it doesn't know what hosts are valid until it hears from them. So due to my order-of-operations in this scenario, the switch hadn't actually heard from R3 at all yet, and the NS is denied.  Making R3 speak solves the problem, which would've worked itself out eventually anyway:

R3-ATTACKER(config)#do ping 2001:600D::2
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 2001:600D::2, timeout is 2 seconds:
Success rate is 100 percent (5/5), round-trip min/avg/max = 0/1/8 ms

Now let's try our theoretical attack.

R3-ATTACKER#ping 2001:600D::100 repeat 1 timeout 0
Type escape sequence to abort.
Sending 1, 100-byte ICMP Echos to 2001:600D::100, timeout is 0 seconds:
Success rate is 0 percent (0/1)
R3-ATTACKER#ping 2001:600D::101 repeat 1 timeout 0
Type escape sequence to abort.
Sending 1, 100-byte ICMP Echos to 2001:600D::101, timeout is 0 seconds:
Success rate is 0 percent (0/1)
R3-ATTACKER#ping 2001:600D::102 repeat 1 timeout 0
Type escape sequence to abort.
Sending 1, 100-byte ICMP Echos to 2001:600D::102, timeout is 0 seconds:
Success rate is 0 percent (0/1)
R3-ATTACKER#ping 2001:600D::103 repeat 1 timeout 0
Type escape sequence to abort.
Sending 1, 100-byte ICMP Echos to 2001:600D::103, timeout is 0 seconds:
Success rate is 0 percent (0/1)

(pretend there are 65,431 hypothetical pings here)

R3-ATTACKER#ping 2001:600D::FFFF repeat 1 timeout 0
Type escape sequence to abort.
Sending 1, 100-byte ICMP Echos to 2001:600D::FFFF, timeout is 0 seconds:
Success rate is 0 percent (0/1)

We expected that outcome as these aren't valid hosts, but what happened on the switch?

*Jul  5 16:18:26.226: %SISF-4-RESOLUTION_VETO: Resolution vetoed NS for D=2001:600D::100 on I=Vl200 reason=Destination not active on link
*Jul  5 16:18:34.146: %SISF-4-RESOLUTION_VETO: Resolution vetoed NS for D=2001:600D::101 on I=Vl200 reason=Destination not active on link
*Jul  5 16:18:43.338: %SISF-4-RESOLUTION_VETO: Resolution vetoed NS for D=2001:600D::102 on I=Vl200 reason=Destination not active on link
*Jul  5 16:18:50.418: %SISF-4-RESOLUTION_VETO: Resolution vetoed NS for D=2001:600D::103 on I=Vl200 reason=Destination not active on link
*Jul  5 16:18:58.958: %SISF-4-RESOLUTION_VETO: Resolution vetoed NS for D=2001:600D::FFFF on I=Vl200 reason=Destination not active on link

And that about sums up destination guard.

A few random notes for the wrap-up.

Some useful show commands:

WS-C4948E#sh ipv6 snooping policies
Target               Type  Policy               Feature        Target range
Gi1/2                PORT  policy1              NDP inspection vlan all

This will show you what policies are applied where.

WS-C4948E#sh ipv6 snooping features
Feature name   priority state
NDP inspection    160   READY
Snooping          128   READY
This shows which features are enabled.

And where is all this in the documentation?

Well, at the time of this writing, there's a link to all this directly under the IOS 15.2E, which is what I labbed on! and ... it's a broken link! Uuuuurgh!

Best I could come up with that I could drill-down to:

Switches -> 
3850 -> 
Catalyst 3850-12S-E Switch -> 
Configuration Guides -> 
IPv6 Configuration Library, Cisco IOS XE Release 3SE (Catalyst 3850 Switches) -> 
IPv6 First-Hop Security Configuration Guide, Cisco IOS XE Release 3SE (Catalyst 3850 Series)

That covers just about everything except Source Guard and Prefix Guard, but let's face it, those two features are pretty easy to understand if you have the rest of it down.

Best of luck!



  1. It's 2017 and this is still some of the best IPv6 first-hop-security information on the internet. Thank you so much!

    You chose to use "security-level glean" for the links facing your router so that RAs will make it through. Would you recommend instead using "security-level inspect" so that ND protections are still enforced, and RAs are allowed similar to "glean"?

    1. I just tested "security-level inspect" and it did NOT let RA's get through, so it is NOT a good choices for uplinks to your router.

  2. This is such a great post, thank you so much for all the time you put into this blog. It has helped me countless times while looking for further explanation on so many topics!

  3. I echo what others have said, this is a really good article. One limitation I found that's worth mentioning is that if you have an access switch with etherchannel uplinks, you cannot attach an ipv6 snooping policy to that uplink, it's not supported.

  4. Thanks! This helped me to remember which option is for which situation.

    For 'glean' I like to pretend it's a messed up way of spelling 'learning' to help me remember what it does.

  5. Thanks man god this subject is not only poorly documented it's poorly desiged lets be real so confusing.