<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>RADIUS on Stan Jewhurst | Personal Blog</title>
    <link>https://www.jewhurst.com/tags/radius/</link>
    <description>Recent content in RADIUS on Stan Jewhurst | Personal Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Wed, 09 Sep 2026 14:58:15 +0200</lastBuildDate><atom:link href="https://www.jewhurst.com/tags/radius/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Eduroam, EAP-TLS and a Dropped Packet Fragment</title>
      <link>https://www.jewhurst.com/posts/radius-packet-fragmentation-in-eduroam/</link>
      <pubDate>Wed, 09 Sep 2026 14:58:15 +0200</pubDate>
      
      <guid>https://www.jewhurst.com/posts/radius-packet-fragmentation-in-eduroam/</guid>
      <description>Students from a visiting school could not get onto eduroam using certificates. The cause turned out to be an ACL in our own network quietly dropping the non-initial fragments of large RADIUS packets.</description>
      <content:encoded><![CDATA[<p>Eduroam is one of the services I took on when I joined CERN. It&rsquo;s the thing that lets someone visiting from another university open their laptop, pick the <code>eduroam</code> SSID, and get online with their home account without anyone having to hand them a guest login. When it works, which is almost always, nobody gives it a second thought.</p>
<p>This is about a case where it didn&rsquo;t. A group of students from a secondary school in Italy were due to come and visit, and on every previous visit they had been unable to connect. It had been raised, looked at, and shelved several times over about two years - each time the students went home before anyone got to the bottom of it, and the ticket went quiet again. This time I had an expired test certificate from the school to work with, and a contact at <a href="https://www.switch.ch/">SWITCH</a>, the Swiss NREN (National Research and Education Network), who was willing to help from their side.</p>
<p>TL;DR: an ACL in our own network was letting the first fragment of a fragmented RADIUS packet through and silently dropping everything after it. Certificate authentication produces packets big enough to fragment and password authentication doesn&rsquo;t, which is why one worked and the other didn&rsquo;t.</p>
<p>IPs, internal hostnames and the school&rsquo;s name are all placeholders below.</p>
<hr>
<h2 id="how-an-eduroam-login-gets-to-where-its-going">How an eduroam login gets to where it&rsquo;s going</h2>
<p>If you already know how eduroam roaming works you can skip this bit. If you&rsquo;ve only ever used it, here&rsquo;s the shape of it:</p>
<p>Eduroam is a large federated RADIUS network. When you authenticate at somewhere you&rsquo;re visiting, your credentials aren&rsquo;t checked there - the visited site has no idea who you are or what your password is. Instead it looks at your realm (the <code>@school.example.it</code> part of your identity), works out that it isn&rsquo;t local, and proxies the request off towards your home institution, which is the only party that can actually authenticate you.</p>
<p>At CERN we run the Service Provider (SP) end of that - the RADIUS servers our access points talk to. Anything that isn&rsquo;t a <code>cern.ch</code> realm gets handed to SWITCH. From there it goes up to the European top-level servers, across to GARR (the Italian NREN), and finally to the school itself. The <code>Access-Accept</code> or <code>Access-Reject</code> comes back down the same chain of proxies.</p>
<pre tabindex="0"><code>  your device
      |          EAP over 802.1X
  CERN SP                &lt;- us
      |          RADIUS over UDP, routed on the realm
  SWITCH  (Swiss NREN)
      |
  eduroam European top-level servers
      |
  GARR  (Italian NREN)
      |
  the school&#39;s IdP       &lt;- actually checks the credentials
</code></pre><p>Two things about this matter for the rest of this post.</p>
<p>The first is that RADIUS between proxies runs over UDP, historically on port 1812. There&rsquo;s no session and no concept of retransmitting part of a message - if a packet doesn&rsquo;t arrive intact, the whole request just times out, and the proxy that sent it eventually decides the next server along is dead.</p>
<p>The second is that certificate authentication produces much larger packets than password authentication. A username and password is a handful of bytes. EAP-TLS runs a full TLS handshake inside the EAP exchange, certificates and all, and the server&rsquo;s half of that - its certificate, any intermediates, the key exchange - is big enough that the RADIUS packet carrying it can go over the usual 1500 byte MTU and end up fragmented at the IP layer.</p>
<p>There&rsquo;s a wrinkle here that cost me the best part of a day. RADIUS has its own mechanism for moving large EAP payloads around: the server hands over the TLS data a slice at a time, one slice per <code>Access-Challenge</code>, and the client acknowledges each slice before the next is sent. That&rsquo;s EAP fragmentation, and it&rsquo;s the long run of <code>Request, TLS EAP</code> / <code>Response, TLS EAP</code> frames you see in a capture of any EAP-TLS handshake.</p>
<p>The catch is that EAP fragmentation only happens on the server that&rsquo;s terminating the TLS session. A proxy in the middle - which is all CERN is here - doesn&rsquo;t reassemble the EAP payload and re-slice it, it just forwards the RADIUS attributes it was given. So if one of those forwarded packets is over the MTU, it&rsquo;s down to ordinary IP fragmentation to get it there in one piece, and IP fragmentation is far less reliable than most people assume.</p>
<hr>
<h2 id="password-works-certificate-doesnt">Password works, certificate doesn&rsquo;t</h2>
<p>There were no live authentication attempts from the school&rsquo;s realm to look at - the ticket had been raised ahead of the visit - so the first thing to do was reproduce it. I set up a local supplicant with the expired certificate and ran two test authentications against the school&rsquo;s realm: one with a deliberately wrong password, one with the certificate.</p>
<p>The password attempt failed, but it failed in the right way:</p>
<pre tabindex="0"><code>(1) Received Access-Reject Id 160 from &lt;FTLR1&gt;:1812 to &lt;CERN-SP&gt;:37304 length 63
(1)   Reply-Message = &#34;Request Denied&#34;
(1) Login incorrect (Home Server says so): [eduroam-test@school.example.it]
</code></pre><p><code>Home Server says so</code>. The request went all the way to Italy, the school&rsquo;s IdP turned down the bad password, and the rejection came all the way back. The federation path is fine.</p>
<p>The certificate attempt failed differently:</p>
<pre tabindex="0"><code>(5) ERROR: Failing proxied request for user &#34;eduroam-test@school.example.it&#34;, due to lack of any response from home server &lt;FTLR1&gt; port 1812
(5) Login incorrect (Home Server failed to respond): [eduroam-test@school.example.it]
</code></pre><p><code>Home Server failed to respond</code>, and on our own RADIUS server the giveaway line:</p>
<pre tabindex="0"><code>Proxy: Marking home server &lt;FTLR1&gt; port 1812 as zombie (it has not responded in 20.000000 seconds).
</code></pre><p>Same user, same realm, same route, tested within a minute of each other. The small packets get an answer and the large ones disappear. That matched the notes from the previous rounds of this ticket, where a colleague had seen exactly the same split.</p>
<p>So: a size problem, on a path that runs from Geneva to Italy and back. Not a small area to search.</p>
<hr>
<h2 id="ruling-things-out">Ruling things out</h2>
<p>SWITCH pointed at two things worth eliminating before going any further.</p>
<p><strong>Were we doing dynamic discovery?</strong> Modern eduroam can skip the proxy chain: the SP looks up a <code>NAPTR</code> record for the realm and, if there is one, connects directly to the home institution&rsquo;s RadSec (RADIUS/TLS) endpoint. The realm does have one:</p>
<pre tabindex="0"><code>$ dig school.example.it NAPTR
school.example.it.  IN  NAPTR  100 10 &#34;S&#34; &#34;x-eduroam:radius.tls&#34; &#34;&#34; _radsec._tcp.eduroam.it.

$ dig _radsec._tcp.eduroam.it SRV
_radsec._tcp.eduroam.it.  IN  SRV  10 0 2083 radius2.garr.net.
_radsec._tcp.eduroam.it.  IN  SRV  20 0 2083 radius.garr.net.
</code></pre><p>If our servers were doing that lookup and then failing to bring up the direct connection, that would fit the symptoms. They aren&rsquo;t:</p>
<pre tabindex="0"><code>$ grep -r &#34;use_naptr&#34; /etc/raddb/
$                       # no output - dynamic discovery is not enabled
</code></pre><p>Our <code>proxy.conf</code> sends <code>cern.ch</code> to the local server and everything else to the SWITCH pool. No dynamic lookups. Ruled out.</p>
<p><strong>Was the certificate chain too big for RADIUS?</strong> A RADIUS packet can&rsquo;t be larger than 4096 octets, full stop. A large enough certificate chain would break that limit and produce a malformed packet rather than a fragmented one. The test certificate&rsquo;s chain was a leaf and one intermediate - nowhere near it. Ruled out.</p>
<p><strong>Does fragmentation work on this path at all?</strong> This is the one that nearly sent me off in the wrong direction. Our network runs a 1500 byte MTU and fragmentation looked completely healthy:</p>
<pre tabindex="0"><code>$ ping -s 1472 -M do &lt;FTLR1&gt;          # 1500 bytes on the wire, don&#39;t fragment
1480 bytes from &lt;FTLR1&gt;: icmp_seq=1 ttl=51 time=5.48 ms

$ ping -s 9000 -M dont &lt;FTLR1&gt;        # 9028 bytes, allow fragmentation
9008 bytes from &lt;FTLR1&gt;: icmp_seq=1 ttl=51 time=6.12 ms
</code></pre><p>A 9000 byte ping - six fragments - to SWITCH&rsquo;s server and back in 6ms. As far as ICMP was concerned there was nothing wrong with fragmentation on this path. That turned out to be worth nothing at all, but I didn&rsquo;t know it yet.</p>
<hr>
<h2 id="a-packet-that-fragments-on-demand">A packet that fragments on demand</h2>
<p>The thing that actually moved this forward was a test packet SWITCH sent over. It&rsquo;s a RADIUS <code>Status-Server</code> request padded out with a stack of <code>Proxy-State</code> attributes until it&rsquo;s certain to be over the MTU. <code>Proxy-State</code> is useful here because the RADIUS spec says a server has to echo every one back untouched, so a single request and its reply put a large packet on the wire in both directions.</p>
<pre tabindex="0"><code>echo &#34;Message-Authenticator = 0x00, \
  Proxy-State = &#39;this-is-SWITCH-testing-for-fragmentation-issues-on-UDP-they-are-truly-annoying-and-affecting-EAP-authentication-in-RADIUS-quite-a-bit&#39;, \
  Proxy-State = &#39;sometimes-fragmentation-issues-show-up-sometimes-they-dont-it-may-depend-on-the-RADIUS-software-used-or-the-EAP-type-itself&#39;, \
  Proxy-State = &#39;for-instance-EAP-TLS-has-a-high-payload-originating-from-the-supplicant-causing-larger-packets-to-be-fragmented&#39;, \
  Proxy-State = &#39;...and so on, another dozen of these to push it well over 1500 bytes...&#39;&#34; \
  | radclient -t 2 -r 1 -x &lt;FTLR1&gt;:1812 status &#39;&lt;shared-secret&gt;&#39;
</code></pre><p>Same 20 second timeout as the certificate authentication. No reply, no echo, nothing.</p>
<p>That took eduroam, EAP and certificates out of the picture. What was left was much simpler: a large UDP packet from our RADIUS server to SWITCH doesn&rsquo;t arrive, and I could now cause that with one command rather than driving a whole EAP-TLS handshake each time.</p>
<hr>
<h2 id="on-a-packet-capture">On a packet capture</h2>
<p>With a one-line reproducer I could capture at both ends of the path and compare. I picked two points: the QA RADIUS server&rsquo;s own interface, and our external firewall, which is the last CERN device the traffic passes before it leaves for SWITCH.</p>
<h3 id="leaving-the-radius-server">Leaving the RADIUS server</h3>
<pre tabindex="0"><code>No.  Time       Source   Destination  Proto   Len   Info
 1   0.000000   CERN-SP  FTLR1        RADIUS  239   Response, Identity
 2   0.091603   FTLR1    CERN-SP      RADIUS  109   Request, TLS EAP (EAP-TLS)
 3   0.093399   CERN-SP  FTLR1        RADIUS  510   Client Hello
 4   0.134959   FTLR1    CERN-SP      RADIUS  1135  Request, TLS EAP (EAP-TLS)
 5   0.135766   CERN-SP  FTLR1        RADIUS  223   Response, TLS EAP (EAP-TLS)
 6   0.179792   FTLR1    CERN-SP      RADIUS  1135  Request, TLS EAP (EAP-TLS)
 7   0.180772   CERN-SP  FTLR1        RADIUS  223   Response, TLS EAP (EAP-TLS)
 8   0.228454   FTLR1    CERN-SP      RADIUS  1135  Request, TLS EAP (EAP-TLS)
 9   0.229206   CERN-SP  FTLR1        RADIUS  223   Response, TLS EAP (EAP-TLS)
10   0.272691   FTLR1    CERN-SP      RADIUS  780   Server Hello, Certificate, Server Key Exchange, Certificate Request, Server Hello Done
11   0.278662   CERN-SP  FTLR1        RADIUS  1635  Response, TLS EAP (EAP-TLS)
12   3.278936   CERN-SP  FTLR1        RADIUS  1635  Response, TLS EAP (EAP-TLS)
13   9.279371   CERN-SP  FTLR1        RADIUS  1635  Response, TLS EAP (EAP-TLS)
14  21.279833   CERN-SP  FTLR1        RADIUS  1635  Response, TLS EAP (EAP-TLS)
15  41.284379   CERN-SP  FTLR1        RADIUS  112   Status-Server id=133
16  41.289539   FTLR1    CERN-SP      RADIUS   80   Access-Accept id=133
</code></pre><p>The handshake runs normally through the small exchanges. Packet 11 is a 1635 byte RADIUS packet carrying the client&rsquo;s reply to the server&rsquo;s certificate - over the MTU, so it leaves the server as two IP fragments.</p>
<p>Then nothing comes back. The server retransmits the same 1635 byte packet at 3, 9 and 21 seconds (packets 12 to 14), gives up, and at 41 seconds sends a small <code>Status-Server</code> health check instead (packet 15). That gets an immediate <code>Access-Accept</code> (packet 16). The server upstream is alive and reachable - it just never saw the big packet.</p>
<h3 id="arriving-at-the-firewall">Arriving at the firewall</h3>
<pre tabindex="0"><code>No.  Time       Source   Destination  Proto  Len   Info
 1   0.000000   CERN-SP  FTLR1        RADIUS 243   Response, Identity
 2   0.091050   FTLR1    CERN-SP      RADIUS 113   Request, TLS EAP (EAP-TLS)
 3   0.093393   CERN-SP  FTLR1        RADIUS 514   Client Hello
 4   0.134523   FTLR1    CERN-SP      RADIUS 1139  Request, TLS EAP (EAP-TLS)
 5   0.135756   CERN-SP  FTLR1        RADIUS 227   Response, TLS EAP (EAP-TLS)
 6   0.179366   FTLR1    CERN-SP      RADIUS 1139  Request, TLS EAP (EAP-TLS)
 7   0.180759   CERN-SP  FTLR1        RADIUS 227   Response, TLS EAP (EAP-TLS)
 8   0.227960   FTLR1    CERN-SP      RADIUS 1139  Request, TLS EAP (EAP-TLS)
 9   0.229199   CERN-SP  FTLR1        RADIUS 227   Response, TLS EAP (EAP-TLS)
10   0.272257   FTLR1    CERN-SP      RADIUS 784   Server Hello, Certificate, Server Key Exchange, Certificate Request, Server Hello Done
11   0.278963   CERN-SP  FTLR1        IPv4   1518  Fragmented IP protocol (proto=UDP 17, off=0, ID=787d)
12   3.279263   CERN-SP  FTLR1        IPv4   1518  Fragmented IP protocol (proto=UDP 17, off=0, ID=7e69)
13   9.279759   CERN-SP  FTLR1        IPv4   1518  Fragmented IP protocol (proto=UDP 17, off=0, ID=85bd)
14  21.280161   CERN-SP  FTLR1        IPv4   1518  Fragmented IP protocol (proto=UDP 17, off=0, ID=9a54)
</code></pre><p>Same test, one hop further along. Identical up to packet 10, and then, in place of a decodable 1635 byte RADIUS packet, the firewall sees this:</p>
<pre tabindex="0"><code>Fragmented IP protocol (proto=UDP 17, off=0, ID=787d)
</code></pre><p><code>off=0</code> is the first fragment, and it&rsquo;s the only fragment that arrives. Wireshark can&rsquo;t decode it as RADIUS because the second fragment never turns up. Every retransmission comes through the same way - one first fragment, a fresh IP ID, nothing after it. Something between the RADIUS server and the firewall was passing fragment one and dropping the rest.</p>
<h3 id="the-acl">The ACL</h3>
<p>My colleague and I walked the path and found a router with an ACL on it - we use these a lot, so it wasn&rsquo;t that surprising. However it was only matching on protocol (UDP) and port (1812), and ACLs are stateless, so any packets <em>had</em> to be an exact match.</p>
<p>Only the first fragment of a fragmented IP packet carries the Layer 4 header. Fragment one of ours had source and destination port 1812 on it, matched the permit rule and went through. Every fragment after it carries no port information at all, so it couldn&rsquo;t match a port-based rule, hit the implicit deny at the end of the list, and was dropped.</p>
<p>This is a well-known way for stateless packet filters and fragmented UDP to go wrong, and RADIUS with EAP-TLS is close to an ideal trigger for it - everything works fine for people using passwords, so most of the users have no issues. It&rsquo;s only more recently that certificates are starting to become more common for eduroam deployments (~20%).</p>
<p>This also explained why CERN certificate continued to work on remote sites - the ACL was only in one direction, and so didn&rsquo;t affect incoming eduroam authentications from abroad.</p>
<h3 id="the-fix">The fix</h3>
<p>The change was small - an ACL entry permitting IP traffic (all of it, not just UDP/1812) between the QA RADIUS server and SWITCH&rsquo;s server, so that fragments with no port information still match on address alone.</p>
<p>Same reproducer, same capture point, after the change:</p>
<pre tabindex="0"><code>No.  Time      Source   Destination  Proto  Len   Info
 1   0.000000  CERN-SP  FTLR1        RADIUS 243   Response, Identity
 2   0.058856  FTLR1    CERN-SP      RADIUS 113   Request, TLS EAP (EAP-TLS)
 3   0.061266  CERN-SP  FTLR1        RADIUS 514   Client Hello
 4   0.110684  FTLR1    CERN-SP      RADIUS 1139  Request, TLS EAP (EAP-TLS)
 5   0.112285  CERN-SP  FTLR1        RADIUS 227   Response, TLS EAP (EAP-TLS)
 6   0.162849  FTLR1    CERN-SP      RADIUS 1139  Request, TLS EAP (EAP-TLS)
 7   0.164506  CERN-SP  FTLR1        RADIUS 227   Response, TLS EAP (EAP-TLS)
 8   0.219533  FTLR1    CERN-SP      RADIUS 1139  Request, TLS EAP (EAP-TLS)
 9   0.221169  CERN-SP  FTLR1        RADIUS 227   Response, TLS EAP (EAP-TLS)
10   0.264564  FTLR1    CERN-SP      RADIUS 784   Server Hello, Certificate, Server Key Exchange, Certificate Request, Server Hello Done
11   0.271370  CERN-SP  FTLR1        IPv4   1518  Fragmented IP protocol (proto=UDP 17, off=0, ID=cd35) [Reassembled in #12]
12   0.271367  CERN-SP  FTLR1        RADIUS 159   Response, TLS EAP (EAP-TLS)
13   0.271573  CERN-SP  FTLR1        IPv4   1518  Fragmented IP protocol (proto=UDP 17, off=0, ID=cd35) [Reassembled in #14]
14   0.271578  CERN-SP  FTLR1        RADIUS 159   Response, TLS EAP (EAP-TLS)
15   0.318409  FTLR1    CERN-SP      RADIUS 113   Request, TLS EAP (EAP-TLS)
16   0.320060  CERN-SP  FTLR1        RADIUS 760   Certificate, Client Key Exchange, Certificate Verify, Change Cipher Spec, Encrypted Handshake Message
17   0.379062  FTLR1    CERN-SP      RADIUS 109   Failure
</code></pre><p>The fragments arrive together now. Wireshark reassembles the 1635 byte packet (<code>[Reassembled in #12]</code>) and decodes the RADIUS layer again (packet 12). The handshake carries on from where it had been stalling: the home server asks for the client&rsquo;s half of the exchange (packet 15), the client sends its certificate and key exchange (packet 16), and it finishes - at 0.38 seconds rather than never - with an EAP <code>Failure</code> (packet 17).</p>
<p>The <code>Failure</code> is the right outcome: the test certificate is expired, so the school is correct to turn it down. What matters is that the authentication ran all the way to the end. A real student with a valid certificate gets an <code>Access-Accept</code> there instead.</p>
<p>The same change then went onto the production eduroam servers.</p>
<hr>
<p>The part of this I&rsquo;ll remember is how convincingly the ping test lied. A 9000 byte ICMP echo crossed the path without any trouble, and if I&rsquo;d taken that at face value I&rsquo;d have ruled out the network early and spent a lot longer than I did reading through RADIUS configs. Fragmented UDP to a particular port on a particular host is its own test, and an ICMP echo to the same host isn&rsquo;t a substitute for it.</p>
<p>The proper fix for this whole class of problem is RadSec - RADIUS over TLS, on TCP port 2083 - where TCP handles segmentation and there are no UDP fragments to lose in the first place. A good part of the eduroam infrastructure already runs on it, and the more of the path that does, the less room there is for something like this. That&rsquo;s a larger piece of work though, and the ACL change sorted out the immediate problem.</p>
<p>Another great result of this is that we saw a 90% drop in eduroam authentication failures for vistors and collaborators from other institutions whose eduroam had been silently failing for all this time!</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
