<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>
    slaac on ForwardingPlane.net
    
    </title>
    <link>https://forwardingplane.net/tags/slaac/</link>
    <description>Recent content 
    
    in slaac on ForwardingPlane.net
    </description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    
    
    <copyright>Copyright (c) 2025, all rights reserved.</copyright>
    <lastBuildDate>Wed, 11 Nov 2026 00:00:00 +0000</lastBuildDate>
    
    
        <atom:link href="https://forwardingplane.net/tags/slaac/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>ipv6-slaac-logger - accountability for SLAAC addresses with RFC 9686</title>
      <link>https://forwardingplane.net/post/2026-11-11-ipv6-slaac-logger/</link>
      <pubDate>Wed, 11 Nov 2026 00:00:00 +0000</pubDate>
      
      <guid>https://forwardingplane.net/post/2026-11-11-ipv6-slaac-logger/</guid>
      <description>&lt;p&gt;One of the most common objections I hear to SLAAC, and frankly one of the most legitimate ones, is accountability. &amp;ldquo;If the host picks its own address, how do I know who had &lt;code&gt;3fff:db8:10::5c1e:9aff:fe3b:21aa&lt;/code&gt; last Tuesday at 21:00?&amp;rdquo; With DHCPv6 IA_NA you get a lease log - this is what everyone is used to with legacy IP - and why so many enterprises ask for DHCPv6 support. With SLAAC, and especially with temporary/privacy addresses rotating underneath you, you get&amp;hellip; whatever you can scrape out of neighbor caches before they age out. We talked about exactly this visibility gap on &lt;a href=&#34;https://packetpushers.net/podcasts/ipv6-buzz/ipb208-ipv6-address-management-is-broken/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;IPv6 Buzz 208&lt;/a&gt; , and it&amp;rsquo;s a real blocker for a lot of security and compliance teams.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://datatracker.ietf.org/doc/rfc9686/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;RFC 9686&lt;/a&gt;  (Registering Self-Generated IPv6 Addresses Using DHCPv6) is the standards-track answer: a host that builds its own address tells a DHCPv6 server about it with an &lt;code&gt;ADDR-REG-INFORM&lt;/code&gt; message, and the server logs it - operators get an analog to what they had with legacy IP, for the most part. The problem is that the client side barely exists at this time. Kea has server support, Android rollout has been announced, systemd-networkd has an open PR, and macOS and Windows don&amp;rsquo;t have it at all. Seeing a significant missing piece here, I built &lt;a href=&#34;https://github.com/buraglio/ipv6-slaac-logger&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;ipv6-slaac-logger&lt;/a&gt;  to close that gap now, instead of waiting for every OS vendor to catch up. Do I expect anyone to roll this out in production at scale? Probably not. However, it is a working implementation that they can point to and tell their vendors &amp;ldquo;we need that as a native feature, here is an example&amp;rdquo;.&lt;/p&gt;
&lt;h2 id=&#34;what-it-does&#34;&gt;What it does&lt;/h2&gt; &lt;p&gt;The goal is simple to state: &lt;strong&gt;for any IPv6 address seen during a time window, answer which DUID, MAC(s) and hostname held it, from when to when, and what evidence each claim rests on.&lt;/strong&gt; It&amp;rsquo;s written in Go, pure Go with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;, and ships as two binaries.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;collector&lt;/strong&gt; - an RFC 9686 server, an option 148 responder, a passive ND/RA sniffer, a SQLite store, syslog forwarding, and a query CLI. Linux only for now. Pretty easily ported to windows if someone were so inclined or I get a few hours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;agent&lt;/strong&gt; - a small user-space RFC 9686 client for &lt;strong&gt;Linux, macOS, and Windows&lt;/strong&gt;. It finds the host&amp;rsquo;s self-generated addresses (SLAAC, temporary, and static, ULA included) and registers each one with the collector.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So there are three ways an address ends up in the database:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Native RFC 9686 clients&lt;/strong&gt; register with the collector exactly as they would with any RFC 9686 server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hosts running the agent&lt;/strong&gt; register on behalf of an OS that can&amp;rsquo;t do it yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything else&lt;/strong&gt; (printers, IoT stuff that can actually do IPv6, the thermostat nobody admits to owning) gets picked up by the &lt;strong&gt;sniffer&lt;/strong&gt; watching Neighbor Discovery traffic. These sightings carry MAC ↔ address only, no DUID, but that&amp;rsquo;s still a lot more than nothing.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;  Native RFC 9686 clients ─┐
  Agents (mac/win/linux) ──┼── multicast ff02::1:2:547 ──┐
                           ├── unicast collector:547 ────┼──&amp;gt; collector ──&amp;gt; SQLite (truth) ──&amp;gt; syslog/rsyslog
  Remote links ── relay ───┴── Relay-Forward ────────────┘        ^
  All hosts (incl. IoT) ── ND/RA on the wire ── SPAN/trunk ── sniffer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The collector accepts direct multicast, direct unicast, and relayed registrations, so it works on a single flat lab network or behind DHCPv6 relays on a campus. Full disclosure: relay needs tested. If you test and this is working, please do let me know. If it has problems, let me know that too. I&amp;rsquo;ll get around to testing it eventually.&lt;/p&gt;
&lt;h2 id=&#34;logs-are-the-product&#34;&gt;Logs are the product&lt;/h2&gt; &lt;p&gt;This is a forensic tool, so the output has to fit into what people already run. SQLite is the source of truth, with 400 days of retention by default, and every event is forwarded as JSON with an &lt;code&gt;@cee:&lt;/code&gt; prefix so rsyslog&amp;rsquo;s &lt;code&gt;mmjsonparse&lt;/code&gt; can handle it, and from there your SIEM or whatever. A registration looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2026-11-08T21:14:03.412Z&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;event&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reg.add&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;addr&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;3fff:db8:10::5c1e:9aff:fe3b:21aa&amp;#34;&lt;/span&gt;,
 &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;duid&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;00:03:00:01:5c:1e:9a:3b:21:aa&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;duid_type&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;LL&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mac&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;5c:1e:9a:3b:21:aa&amp;#34;&lt;/span&gt;,
 &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mac_src&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;duid&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;agent&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;host&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lab-mbp&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;via&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;multicast&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;link&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;en0/3fff:db8:10::/64&amp;#34;&lt;/span&gt;,
 &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;preferred_lft&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#ae81ff&#34;&gt;14400&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;valid_lft&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#ae81ff&#34;&gt;86400&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;xid&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;a1b2c3&amp;#34;&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Events cover registrations (&lt;code&gt;reg.add&lt;/code&gt;, &lt;code&gt;reg.change&lt;/code&gt;, &lt;code&gt;reg.expire&lt;/code&gt;, &lt;code&gt;reg.conflict&lt;/code&gt;, &lt;code&gt;reg.reject&lt;/code&gt;), sniffer sightings (&lt;code&gt;nd.new&lt;/code&gt;, &lt;code&gt;nd.conflict&lt;/code&gt;, &lt;code&gt;nd.expire&lt;/code&gt;), and router advertisements (&lt;code&gt;ra.seen&lt;/code&gt;, &lt;code&gt;ra.change&lt;/code&gt;). That last pair turns out to be a nice side benefit: you get a log entry whenever a new router shows up on a link or an existing one flips its M/O flags or prefix set. That&amp;rsquo;s useful for rogue RA detection in its own right.&lt;/p&gt;
&lt;p&gt;Repeat sightings and refreshes only update &lt;code&gt;last_seen&lt;/code&gt; in the database. They never generate syslog lines, so a busy link doesn&amp;rsquo;t bury the events you actually care about, or as a mentor once taught me &amp;ldquo;never alarm on non-actionable stuff&amp;rdquo;. I try to keep that ethos alive.&lt;/p&gt;
&lt;p&gt;Answering the &amp;ldquo;who had this address&amp;rdquo; question is one command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;slaac-logger-collector query -addr 3fff:db8:10::5 -at 2026-11-08T21:00:00Z
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-agent&#34;&gt;The agent&lt;/h2&gt; &lt;p&gt;Getting the agent going is pretty painless:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;slaac-logger-agent list                       &lt;span style=&#34;color:#75715e&#34;&gt;# show local addresses and which would be registered&lt;/span&gt;
slaac-logger-agent -config agent.toml install &lt;span style=&#34;color:#75715e&#34;&gt;# install as a service&lt;/span&gt;
slaac-logger-agent start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It runs as a systemd unit with &lt;code&gt;DynamicUser=yes&lt;/code&gt; and only &lt;code&gt;CAP_NET_BIND_SERVICE&lt;/code&gt; on Linux, a launchd daemon on macOS, and a Windows Service as &lt;code&gt;LocalService&lt;/code&gt;. It tries to reuse the OS&amp;rsquo;s own DHCPv6 DUID (from the Windows registry, macOS&amp;rsquo;s &lt;code&gt;DUID_IA.plist&lt;/code&gt;, or dhclient&amp;rsquo;s leases) so that its records line up with any existing DHCPv6 server logs. If it can&amp;rsquo;t, it generates a DUID-LLT and persists it, so the identity survives restarts &lt;em&gt;and&lt;/em&gt; MAC randomization. That last part matters a lot when every phone and laptop on the network is rotating its MAC.&lt;/p&gt;
&lt;p&gt;A few deliberate choices worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Temporary/privacy addresses are registered too.&lt;/strong&gt; They&amp;rsquo;re the ones you most need a record of.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No de-registration.&lt;/strong&gt; This departs from the RFC on purpose. Once the OS removes an address it can&amp;rsquo;t be used as a source, and de-registering at shutdown would falsely claim the host stopped using it. End of use is inferred from &lt;code&gt;last_seen&lt;/code&gt; plus lifetime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The heartbeat defaults to 1 hour&lt;/strong&gt;, shorter than the RFC&amp;rsquo;s lifetime-based refresh, to bound the uncertainty about when an address stopped being used.&lt;/li&gt;
&lt;li&gt;The agent identifies itself in Vendor Class (option 16) with enterprise number &lt;strong&gt;67611&lt;/strong&gt;, the IANA PEN assigned to this project. That marker is advisory, since anyone can spoof a Vendor Class, and the logs record &lt;code&gt;origin&lt;/code&gt; with that in mind.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-sniffer&#34;&gt;The sniffer&lt;/h2&gt; &lt;p&gt;Give a collector interface the &lt;code&gt;sniff&lt;/code&gt; role and it watches DAD, NS, NA, and RS traffic to learn MAC ↔ address pairs. A few things I learned building it that are worth passing along:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MLD snooping will hide most of what you want to see.&lt;/strong&gt; On an access port, most DAD and solicited-node traffic never reaches you. Use a SPAN/mirror port or a trunk.&lt;/li&gt;
&lt;li&gt;It reads 802.1Q tags the kernel strips into socket metadata (raw &lt;code&gt;AF_PACKET&lt;/code&gt; with &lt;code&gt;PACKET_AUXDATA&lt;/code&gt;), so VLANs are recorded correctly, plus one in-frame tag for QinQ (not fully tested).&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s built to survive hostile links. Live sightings are capped (100k by default), event rates are token-bucketed, and a MAC claiming more than 32 addresses on a link is flagged as a probable proxy-ND router rather than flooding the logs. Conflicts, where an address moves to a new MAC, are logged as &lt;code&gt;nd.conflict&lt;/code&gt; with the previous MAC attached.&lt;/li&gt;
&lt;li&gt;All of this is exported as counters in a periodic &lt;code&gt;stats&lt;/code&gt; event, so you can tell &amp;ldquo;nothing happened&amp;rdquo; apart from &amp;ldquo;the sniffer can&amp;rsquo;t see anything.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;caveats&#34;&gt;Caveats&lt;/h2&gt; &lt;p&gt;This is new, and I want to be upfront about the edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The collector, including the sniffer, is &lt;strong&gt;Linux only&lt;/strong&gt; for now. The agent is cross-platform.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native clients only register after a DHCPv6 server hands them option 148.&lt;/strong&gt; If there&amp;rsquo;s no other DHCPv6 server on the link, give the collector the &lt;code&gt;responder&lt;/code&gt; role and &lt;strong&gt;set the O flag in your RAs&lt;/strong&gt;. Don&amp;rsquo;t run the responder on the same link as a Kea server that also answers registrations, or you&amp;rsquo;ll get two servers answering.&lt;/li&gt;
&lt;li&gt;On Linux, the agent only reads dhclient&amp;rsquo;s DUID. On systemd-networkd or NetworkManager hosts, set &lt;code&gt;duid&lt;/code&gt; in the config if you need it to match.&lt;/li&gt;
&lt;li&gt;Windows can&amp;rsquo;t tell temporary addresses apart from stable-privacy ones, so both are reported as &lt;code&gt;slaac&lt;/code&gt;. Both are still registered.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;CGO_ENABLED&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; go build -o bin/slaac-logger-collector ./cmd/collector
CGO_ENABLED&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; go build -o bin/slaac-logger-agent ./cmd/agent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a unit test suite, a Linux network-namespace end-to-end test, and a CI interop job that runs the agent against ISC Kea 3.0, so it isn&amp;rsquo;t only tested against itself. As with some of my other recent projects, LLM tooling, specifically Claude was used during development to help me make my own code &amp;ldquo;suck less&amp;rdquo;, and to aid in some of the more complex things I am still learning, and a human reviewed all of it - I want to learn as much as I can from &amp;ldquo;companion coding&amp;rdquo;; the README says so plainly.&lt;/p&gt;
&lt;h2 id=&#34;why-bother&#34;&gt;Why bother?&lt;/h2&gt; &lt;p&gt;For me, the larger point is that &amp;ldquo;we can&amp;rsquo;t track SLAAC addresses&amp;rdquo; has been a reason, or at least an excuse, to keep DHCPv6-only or IPv4-only policies around for far too long. RFC 9686 is the right long-term answer, and as native client support lands this tool should mostly fade into the background as a plain RFC 9686 collector. Until then, an agent plus a sniffer plus a sane log format gets you most of the way there today.&lt;/p&gt;
&lt;p&gt;The code is at &lt;a href=&#34;https://github.com/buraglio/ipv6-slaac-logger&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;github.com/buraglio/ipv6-slaac-logger&lt;/a&gt; . Kick the tires, break it, and open issues. I&amp;rsquo;m especially interested in hearing from anyone testing native Android registrations or the systemd-networkd client.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
