pf divert to modify ICMPv6 packet

Hi everyone,

I'm on 11.2-STABLE and I'm trying to modify an ICMPv6 packet via a divert socket. The rationale is simple: The router that UPC (Swiss cable company) ships is, frankly, terrible and sends its own DNS servers as part of the ICMPv6 router advertisement. I don't want to use them. Since I already have bridged IPv6 and NATed IPv4 (that rubbish box can't assign IPv4 address ranges outside of 192.168.x.y... sigh) and there is no way to configure the DNS servers on the router, I'm stripping those fields. Should be simple enough, right? Unfortunately, I was unsuccessful so far and I think there are multiple blockers. Let me start with the pf rule I'm using:

Code:
pass in log on $ext_if inet6 proto icmp6 all icmp6-type routeradv divert-to 134

This should divert ICMPv6 router advertisements to a divert socket on port 134. But that's when dmesg starts yelling at me:

Code:
pf: divert(9) is not supported for IPv6

Question #1: Is there any way to enable support or is this simply not supported at all?

But even if I try to divert other arbitrary IPv4 packets, I'm not receiving them in my purge script (see below).

Question #2: Any hints on why my purge script isn't picking up anything? kldload tells me that ipdivert is already loaded or in kernel. pflog does contain the packets, so the rule apparently works.

Cheers
Lennart

---

Python:
#!/usr/bin/env python3
import socket
import struct
import sys

try:
    socket.IPPROTO_DIVERT
except AttributeError:
    socket.IPPROTO_DIVERT = 254


NATIVE_U16_STRUCT = struct.Struct('@H')
NETWORK_U16_STRUCT = struct.Struct('!H')
NEXT_HEADER_U16, *_ = NATIVE_U16_STRUCT.unpack(b'\x00\x3a')

def recalculate_checksum(view: memoryview, array: bytearray) -> None:
    checksum = 0

    # Reset checksum to zero
    NATIVE_U16_STRUCT.pack_into(array, 42, 0)

    # Pseudo-header: Source/Destination
    checksum += sum(value for value, *_ in NATIVE_U16_STRUCT.iter_unpack(view[8:40]))
    # Pseudo-header: two zero bytes (ignored), followed by the length
    payload_length, *_ = NATIVE_U16_STRUCT.unpack_from(view, 4)
    checksum += payload_length
    # Pseudo-header: three zero bytes (ignored), followed by
    # next header field (in 16-bit representation, native decoding)
    checksum += NEXT_HEADER_U16
    
    # Calculate checksum for ICMPv6 payload
    checksum += sum(value for value, *_ in NATIVE_U16_STRUCT.iter_unpack(view[40:]))

    # Add carry bits to checksum
    checksum = (checksum >> 16) + (checksum & 0xffff)
    checksum += (checksum >> 16)

    # Invert checksum
    checksum ^= 0xffff

    # Update checksum
    NATIVE_U16_STRUCT.pack_into(array, 42, checksum)


def handle(view: memoryview, array: bytearray) -> memoryview:
    # IPv6 header + ICMPv6 *Router Advertisement* require at least 56 bytes
    if len(view) < 56:
        return view
    
    # Ensure next header is an ICMPv6 packet
    if view[6] != 58:
        return view
    
    # Ignore ICMPv6 messages that are not the router advertisement
    if view[40] != 134:
        return view

    # Disable the *MO* flags
    # (*managed address configuration*/*other configuration*)
    array[45] &= 0x3f

    # Go through options...
    offset = 56
    while True:
        option = view[offset:]
        
        # Option header has 2 bytes
        if len(option) < 2:
            recalculate_checksum(view, array)
            return view

        # Validate length
        length = option[1] * 8
        if len(option) < length:
            recalculate_checksum(view, array)
            return view

        # Strip DNS servers
        if option[0] == 25:
            # Move remaining ICMP options forward and update view
            remaining = view[offset + length:]
            array[offset:offset + len(remaining)] = remaining
            view = view[:len(view) - length]
        
            # Update payload length
            NETWORK_U16_STRUCT.pack_into(array, 4, len(view) - 40)
            print('STRIPPED!');
        else:
            offset += length


def main():
    # Create socket
    with socket.socket(family=socket.AF_INET, type=socket.SOCK_RAW, proto=socket.IPPROTO_DIVERT) as fd:
        # Bind socket
        fd.bind(('0.0.0.0', 134))

        # Create buffer
        array = bytearray(4096)
        view = memoryview(array)

        # Enter loop
        while True:
            # Receive packet
            n_read, address = fd.recvfrom_into(view)
            in_view = view[:n_read]
            
            # Modify packet (if needed)
            print('HANDLE!');
            try:
                out_view = handle(in_view, array)
            except Exception as exc:
                print('Could not modify packet: {}'.format(exc), file=sys.stderr)
            
            # Send packet
            fd.sendto(out_view, address)


if __name__ == '__main__':
    main()
 
The divert(4) is part of IPFW, not PF.

I'm fairly certain the UPC box can be put in bridge mode instead of router mode. When it's in bridge mode you can do all the DHCP, DNS, etc. on your FreeBSD router.
 
Thanks for your answer.

Where does pf's divert-to end up then?

The UPC box can be put into bridge mode but that disables IPv6 which isn't an option for me.
 
Where does pf's divert-to end up then?
It's part of PF itself.
Code:
     divert-to <host> port <port>
           Used to redirect packets to a local socket bound to host and port.
           The packets will not be modified, so getsockname(2) on the socket
           will return the original destination address of the packet.

The UPC box can be put into bridge mode but that disables IPv6 which isn't an option for me.
I have a similar issue. My ISP doesn't even provide IPv6 natively. But resolved it by getting an IPv6 tunnel from HE.net.
 
It's part of PF itself.
Code:
     divert-to <host> port <port>
           Used to redirect packets to a local socket bound to host and port.
           The packets will not be modified, so getsockname(2) on the socket
           will return the original destination address of the packet.

I'm confused by this. Redirect packets to a local socket bound to host and port. I'd interpret local as a local socket on the machine. But what type of socket are we talking about? What protocol? What address family? Having glimpsed over the source code, I have a hunch this actually does work like divert for ipfw, it's just badly documented.

I have a similar issue. My ISP doesn't even provide IPv6 natively. But resolved it by getting an IPv6 tunnel from HE.net.

Makes sense. But for my scenario, it would be silly to tunnel IPv6 just because I struggle to modify an ICMPv6 packet.
 
Lets try a different avenue. How are you getting an IPv6 address and how are you providing it for the rest of your network?

For example, I'm getting DNS settings from my ISP due to dhclient(8). But I configured it to ignore the DNS settings as I have my own DNS services. I'm sure we can come up with something similar for IPv6.
 
I'm using SLAAC. I can easily tell my Linux machine to ignore the DNS addresses but every other device in the network would have to do the same.
 
Then how is that divert supposed to work? You could only divert it to a local port or socket but all your other machines in your network would get that same RA from the UPC box. Even if you manage to bounce out a new RA from the FreeBSD machine you'd have a problem on your other machines as they would then receive two RAs, one from the UPC box and one from your FreeBSD box.

It sounds like your FreeBSD machine is on the same network and not 'in between' your network and the UPC box. Am I understanding that correctly?
 
The FreeBSD machine is in between. It bridges IPv6 while NATing IPv4.

I've now rewritten my rules to use ipfw since IPv6 diversion simply is not implemented in pf. There was also a typo in my Python code (IPPROTO_DIVERT is 258, not 254). Now, the code receives and strips the packet but it still appears unstripped on the wire. I'm currently investigating.
 
It doesn't look like there is a way to make this work with divert sockets over a bridge. I can filter the router advertisement for the local machine, and that works, but I cannot filter it for the purpose of forwarding. I always get no route to host which may happen since it looks like a bunch of meta information is being lost when sending an IPv6 packet from the divert socket back to the outbound queue.

So, I guess I'll have to live with UPC's DNS servers... so close, so very disappointing... 😞
 
Back
Top