Re: pf.c: IPv6 Fragmentation Tag Not Cleared after Reassembly

Zixu Wu <[email protected]>
Newsgroups gmane.os.openbsd.bugs
Message-ID <[email protected]>
Hello Sashan,

Thank you for looking into this matter.

Let's modify the scenario you have outlined slightly, so that instead of 
double reassembly/refragmentation, we only have one reassembly and one 
refragmentation.

> to get to this code path one needs to set wg(4) mtu to 9k and try to
> send ICMPv6 echo-requests 32k big. then we should get to 'double
> fragmentation' path:
> 
> 	32k -> 9k -> ~1.5k (physical ethernet)

Let's say the ICMPv6 echo-request is 1k instead of 32k, and a buggy 
WireGuard client appends a ridiculous amount of zero paddings to the 
ICMPv6 packet right before encapsulation, so the outer packet is 9k.

> 
> receiving reply goes in reverse direction:
> 
>      ip6_input_if() on ethernet
> 	pf_test(PF_IN, 1.5k)
> 	    add PACKET_TAG_PF_REASSEMBLED
> 	    reassemble packet to 9k UDP datagram with wg(4) payload

This step remains the same

> 	ip_ours()
> 	    wg_input()
> 		the PACKET_TAG_PF_REASSEMBLED is left at mbuf
> 		pf_pkt_addr_changed(m)
> 		put packet to wg_deliver_in() task which calls ip6_input_if()

This step also remains the same except that after wg_decap() discards 
the padding, the inner packet with a size of only 1k, is a 
straight-forward IPv6 packet without fragmentation.
> the ip6_input_if() task then does:
> 
>      pf_test(PF_IN, 9k)
> 
> 	add PACKET_TAG_PF_REASSEMBLED after 9k frags are
> 	reassembled to 32k ICMPv6 fragment. my understanding
> 	is the packet has two PACKET_TAG_PF_REASSEMBLED tags now.
> 	one for ethernet, the other for wireguard.

Since the inner ICMPv6 packet is only 1k with no fragmentation, a second 
PACKET_TAG_PF_REASSEMBLED is not added.

>      ip_ours() for local bound packets or ip6_forward() for forwarding
>      local bound traffic should should be fine here.
> 
>      it's forwarding case what brings us to code path where we hit
>      error. for forwarding case the IP stack calls pf_test() again
> 
>      pf_test(PF_FWD, 32k). the code reaches the area touched by your diff:
> 
> 8786
> 8787 #ifdef INET6
> 8788         /* if reassembled packet passed, create new fragments */
> 8789         if (pf_status.reass && action == PF_PASS && pd.m && fwdir == PF_FWD &&
> 8790             pd.af == AF_INET6) {
> 8791                 struct m_tag    *mtag;
> 8792
> 8793                 if ((mtag = m_tag_find(pd.m, PACKET_TAG_PF_REASSEMBLED, NULL)))
> 8794                         action = pf_refragment6(&pd.m, mtag, NULL, NULL, NULL);
> 8795         }
> 8796 #endif  /* INET6 */
> 
>       the pf_refragment() creates a chain of brand new packets. the frag sizes
>       match the MTU of outbound interface.

This is where the problem occurs, pf_refragment6() is invoked on the 
inner ICMPv6 packet, using the `struct pf_fragment_tag` created for the 
1.5k link. The 1k ICMPv6 packet is now fragmented by pf.c (to only one 
fragment in total) even though it was originally not fragmented.

At its core, it's a situation where pf_reassemble6() operates on the 
outer encapsulation, whereas the pairing pf_refragment6() operates on 
the inner packet.

To turn it into observable behavior, we can take advantage of this bug: 
https://marc.info/?l=openbsd-bugs&m=178610582493242&w=2

With the standard WireGuard MTU of 1420, a ICMPv6 packet 1409 bytes long 
will do the trick. Payload will need to be 1361:

40 (IPv6 header) + 8 (ICMPv6 header) + 1361 (payload) = 1409

Due to the WireGuard padding bug, the ICMPv6 packet will be padded to 
1424 (1409 + 15) bytes right before encryption. The size that needs to 
down the ethernet interface will be 1504:

40 (IPv6 header) + 8 (UDP header) + 16 (WireGuard header) + \
   1409 (ICMPv6) + 15 (WireGuard Padding) + \
   16 (Wireguard authentication tag) = 1504

This will be fragmented on wire, triggering the problematic pathway on 
the receiving end.

The following script can be used to set the stage. Here we have three 
wg(4) interfaces wg1, wg2, wg3 in rdomain 1, 2, 3 respectively. Among 
those, wg1 is configured to be the router, routing packets between wg2 
and wg3.

----
#!/bin/sh

# mimic standard Ethernet
ifconfig lo0 mtu 1500

# we need pf(4) to not skip lo(0)
pfctl -f - <<EOF
EOF

sysctl net.inet6.ip6.forwarding=1

# create interfaces; set random private keys
ifconfig wg1 create wgport 7111 wgkey `openssl rand -base64 32` rdomain 1
ifconfig wg2 create wgport 7222 wgkey `openssl rand -base64 32` rdomain 2
ifconfig wg3 create wgport 7333 wgkey `openssl rand -base64 32` rdomain 3

# retrieve the public keys associated with the private keys
PUB1="`ifconfig wg1 | grep 'wgpubkey' | cut -d ' ' -f 2`"
PUB2="`ifconfig wg2 | grep 'wgpubkey' | cut -d ' ' -f 2`"
PUB3="`ifconfig wg3 | grep 'wgpubkey' | cut -d ' ' -f 2`"

ifconfig wg1 wgpeer "$PUB2" wgendpoint ::1 7222 wgaip fd00::2/128
ifconfig wg1 wgpeer "$PUB3" wgendpoint ::1 7333 wgaip fd00::3/128
ifconfig wg2 wgpeer "$PUB1" wgendpoint ::1 7111 wgaip ::/0
ifconfig wg3 wgpeer "$PUB1" wgendpoint ::1 7111 wgaip ::/0
ifconfig wg1 inet6 fd00::1/128
ifconfig wg2 inet6 fd00::2/128
ifconfig wg3 inet6 fd00::3/128

route -T1 add -inet6 -iface default fd00::1
route -T2 add -inet6 -iface default fd00::2
route -T3 add -inet6 -iface default fd00::3
----

When packets are observed on wg1:

ping6 -c1 -V2 -Ds 1361 fd00::3

You will see that the packet routed to wg3 will be fragmented (the 
incorrect behavior).

Further, if you increase the payload to 1365:

ping6 -c1 -V2 -Ds 1365 fd00::3

You will see an ICMPv6 Packet Too Big message.

40 (IPv6 header) + 8 (IPv6 extension header for fragmentation) + \
   8 (ICMPv6 header) + 1365 (payload) = 1421 (right above WireGuard MTU)

This ICMPv6 packet should have been fragmented.

Regards,
Zixu

> So this is my understadning. I was trying to think of some observable error.
> but apart from having a packet with two REASSEMBLED tags attached I can not
> think of anything else that goes wrong here.
> 
> I'm more concerned the things may actually break when the tag is deleted
> for PF_IN packets. that's just my gut feeling about it.
> 
> 
> thanks and
> regards
> sashan
> 
> 
> On Sun, Aug 09, 2026 at 04:35:58PM +0000, Zixu Wu wrote:
> </snip>
>> diff --git a/sys/net/pf.c b/sys/net/pf.c
>> index 0fd00c0dbf3..493b7385730 100644
>> --- a/sys/net/pf.c
>> +++ b/sys/net/pf.c
>> @@ -8785,13 +8785,17 @@ done:
>>    }
>>
>>   #ifdef INET6
>> - /* if reassembled packet passed, create new fragments */
>> - if (pf_status.reass && action == PF_PASS && pd.m && fwdir == PF_FWD &&
>> -     pd.af == AF_INET6) {
>> + /* create new fragments if necessary */
>> + if (pd.m && pd.af == AF_INET6) {
>>      struct m_tag  *mtag;
>>
>> -   if ((mtag = m_tag_find(pd.m, PACKET_TAG_PF_REASSEMBLED, NULL)))
>> -     action = pf_refragment6(&pd.m, mtag, NULL, NULL, NULL);
>> +   if ((mtag = m_tag_find(pd.m, PACKET_TAG_PF_REASSEMBLED, NULL))) {
>> +     if (action == PF_PASS && fwdir == PF_FWD) {
>> +       action = pf_refragment6(&pd.m, mtag, NULL, NULL, NULL);
>> +     } else {
>> +       m_tag_delete(pd.m, mtag);
>> +     }
>> +   }
>>    }
>>   #endif /* INET6 */
>>    if (st && action != PF_DROP) {
>>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.