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) {
>>