Weekly update

Week of 2026-07-27

IPsec was the centre of the week. Ze ran against strongSwan and twenty defects came out. EAP-TLS worked with another implementation for the first time. The BGP announce path went from two encoders to one. OSPF, BMP, MRT and the MCP server each took correctness work.

← All weekly updates

🔒 IPsec and IKEv2

Ze's IKEv2 daemon runs against strongSwan in thirteen scenarios. Two of them used to pass whatever happened on the wire, because their checks sat inside a catch-all handler. Removing that made the rest visible.

Interoperability:

  • EAP-TLS had never worked with another implementation. Four defects: a malformed signature, a discarded TLS closing flight, a master session key no other implementation computes, and a client certificate with no subjectAltName. TLS 1.3 works now too.
  • Ze programmed the negotiated 0.0.0.0/0 as a kernel policy for the whole node, with no exemption for its own IKE traffic. It encrypted its own replies.
  • Ze read a UDP port change as proof of a NAT. strongSwan changes port for other reasons and keeps sending plain ESP. One tunnel now accepts both forms.

Selectors and lifecycle:

  • A peer that deleted a Child SA was ignored. Ze kept encrypting to a tunnel the peer had torn down.
  • Closing an IKE SA left its keys in memory.
  • The initiator accepted whatever traffic selectors the responder sent back. Answer 0.0.0.0/0 to a proposal of 10.1.0.0/16, and Ze programmed a policy for the whole internet.
  • The responder answered every Child SA with a full wildcard, then programmed something narrower. Traffic the peer believed was covered was dropped without a word. Both sides read one set now, and it can only get narrower.
  • Rekey failed in the kernel with EEXIST. Cleaning up the old pair removed the survivor's policy. A NAT tunnel lost its encapsulation. A transport-mode tunnel became a tunnel-mode one at its first rekey.
  • Narrowing brought new per-peer settings: mode (tunnel or transport), transport-required, and a traffic-selector list of local and remote prefixes, ports and protocol. No list means allow everything, so nothing changes for an existing configuration.

Certificates and the rest:

  • A certificate chain is bounded on receive as well as on send. An overlong chain is refused, not truncated.
  • dh-group took any number from 1 to 31. Only 14, 19 and 20 exist.
  • An identity containing NUL or CR reached the wire. It is refused now, at commit and on receive.
  • Hash-and-URL certificate fetching is implemented, behind a leaf that defaults to off. The hash is checked before anything else sees the bytes, and the fetcher parses nothing. Around it: an http-only allowlist, a 64 KiB cap, five seconds, no redirects, eight fetches in flight, and a deny list for loopback, private, CGNAT, link-local and metadata addresses applied when the socket is dialled.
  • COOKIE challenges, the INVALID_KE retry and a conforming ECP key format all landed. An authenticated request that fails gets an answer instead of silence.
  • The VPP IPsec backend refuses transport mode and port-restricted selectors. It used to install a tunnel for them and report success.
  • RFC 7296 is fully extracted: 227 requirements, no gaps.

🛰️ BGP on the wire

There is one UPDATE encoder now. Both announce paths describe their changes as edits over the received bytes and build the message once, so a route leaves the daemon as the same bytes whichever path sent it. Payload copies per destination drop from two to one on the general path, and from three to one for a route-server client that needs both a prepend and a 4-byte AS transcode. Destinations with the same policy share one build: a hundred destinations in two policy groups run about 29% faster per destination.

Attributes and policy:

  • session { community { send ... } } was ignored unless it was set to all. Every peer, both paths. CLUSTER_LIST and ORIGINATOR_ID had the same hole.
  • An AS-path prepend on the slow path destroyed a valid AGGREGATOR attribute. Whether it survived depended on which path the route took.
  • An eBGP prepend never cleared the transitive bit.
  • An announce made through the API could carry LOCAL_PREF to eBGP peers. RFC 4271 forbids that.
  • Nine places read an UPDATE's announcements before its withdrawals. A message naming one prefix in both lost the route: in the RIB, in show bgp peer rib, in BMP, in the stored config, in RPKI re-validation, and in the route reflector and the route server. Withdrawals come first everywhere now, as RFC 4271 Section 4.3 asks.
  • A withdrawal carrying no routes, and the End-of-RIB marker itself, were stamped with an Only-To-Customer attribute.
  • Unrecognized typed NLRI is discarded, per RFC 7606 Section 5.4, and the rule is set per family. BGP-LS keeps it, because RFC 9552 says so. EVPN discards types outside 1 to 5.
  • FlowSpec actions written as one word (discard, copy-to-nexthop) work through the API. Only the config parser understood them before.

Sessions, identity and forwarding:

  • A second OPEN on a live session rewrote the negotiated capabilities, so a peer could add Add-Path mid-session. Ze answers with a Cease now.
  • The RFC 6286 identifier checks never ran for a dynamic peer. Separately, every peer without a configured AS claimed its identifier under AS_TRANS, so two peers in different 4-byte AS numbers collided and the second was refused.
  • A route-server client received the route server's own control communities whenever a route carried two or more of them. With one it worked.
  • An UPDATE from a peer whose session was up, but whose state event had not arrived yet, was dropped and never replayed.
  • Thirteen writes into pooled buffers on the announce paths are bounded. An attribute rewrite that grows, or a long list of prefixes, could write into the next buffer.

🛰️ OSPF, BMP and MRT

  • An NSSA border router always originates a default into every attached NSSA now, and installs a received Type-7 default only when its P-bit is set, as RFC 3101 requires. This is the OSPFv2 half. OSPFv3 is not fixed, and the status page says so.
  • Two OSPF paths opened the shared state store from their own process, where the store's lock does not reach. A restore rewrote that file in place, which invalidated other processes' mappings, and the next read killed the daemon.
  • BMP no longer blocks BGP. It wrote to the socket while holding the write lock, so a slow collector stalled the Loc-RIB subscriber. Whole messages go into a bounded queue instead.
  • Stopping a BMP session closed the socket before the Termination message was written. No collector ever received one.
  • A BMP dump closes every address family it owes, the empty ones included. A collector waiting on IPv4 while IPv6 had routes used to wait forever.
  • Each BMP dump carries a token. Two collectors reconnecting at once used to cross-wire their dumps, and one could be told a dump it never asked for had finished.
  • MRT files decoded wrong in four ways: IPv6 next-hops wrong or missing, 2-byte AS paths corrupt, IPv6 prefixes read as 0.0.0.0/0, and add-path dumps giving zero routes in five subcommands.
  • A damaged MRT file reports the record number, type and time. What parsed is kept, so 500 good prefixes are no longer thrown away because the 501st was cut short.

🤖 MCP

The server speaks MCP revision 2026-07-28. It is stateless: no initialize handshake, no protocol sessions, no GET stream, no server-initiated requests. Authentication happens on every request, so a stolen session id is worth nothing on its own. Four defects were fixed on the way:

  • environment.mcp auth mode and token were thrown away unless the block carried a port, so ze --mcp <port> accepted anyone.
  • ze_execute marked command as required, which made the short tool form unusable.
  • Resources were gated on a client capability that does not exist, so no client could read a ui:// asset.
  • A plugin command marked hidden was still advertised as a tool, and each of 106 proxied commands appeared twice.

🛠️ Operations

  • Ze could not shut down once an L2TP PPP session reached the kernel. It hung, and a live LNS needed SIGKILL.
  • show system kernel-log hung the daemon on any host that can read /dev/kmsg.
  • The web config editor could not delete a list entry at all, on any of its three surfaces.
  • delete bgp peer !edge1 deleted edge1. Exclusion selectors are refused on commands that destroy one peer.
  • delete bgp peer 127.0.0.1 was answered with "requires a selector".
  • show tcp-check 127.0.0.1 1 timeout 2s was rejected for a missing port, because positional matching skipped numeric arguments.
  • No plugin metric ever reached the registry. show metrics values listed 38 series and none came from a plugin.
  • Policy-routing table IDs above 2^31 are refused on every build. On a 32-bit build the number went negative and the rule was installed against the wrong table, with no error.
  • ze doctor warns before apply when the running VPP has no linux_cp plugin. Enabling vpp.lcp used to fail the whole apply with a raw VPP error.
  • A hub config with plugins and no BGP could not start an external plugin at all.

📚 Standards ledger

Most of this week's defects came out of one method. Every RFC Ze implements is read requirement by requirement, and a requirement counts as met only when a test names it and fails when the behaviour is removed. Claude Opus 5 does that reading, and most of what is listed above is what it found.

  • The gate now checks the RFC's own text. Every normative sentence is pulled out and classified before the document counts as covered, so the gate can no longer be green over what nobody read. RFC 7296 is the first: 261 sites in 104 sections, and that first pass found four requirements no check could see.
  • 58 source texts were added, including two the BMP work had been done against. Those summaries had been checked against themselves.
  • An interoperability run can prove a requirement now. The runner fails when Docker is missing instead of calling it a pass, and nightly-only evidence is counted in its own column.
  • Four RFCs were published as Supported or Experimental over empty checklists. RFC 4486 turned out to have one requirement, and it is proven. DNS (RFC 1035) and the IS-IS hostname (RFC 5301) have 34 between them that Ze does not meet, and both are listed as debt now.
  • The gate covers 168 documents.

🔭 Coming up

Closing all the deferred work this week opened. It sits in OSPFv3, in IPsec (remote access, the VPP backend, more strongSwan scenarios), in the BGP route server and its dynamic peers, and in the DNS and IS-IS hostname compliance still owed.