Proxying Ethernet Frames to PACKRAT (Part 5/5) 🐀

Paul Tagliamonte 2021-12-06
🐀 This post is part of a series called "PACKRAT". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

In the last post, we left off at being able to send and receive PACKRAT frames to and from devices. Since we can transport IPv4 packets over the network, let’s go ahead and see if we can read/write Ethernet frames from a Linux network interface, and on the backend, read and write PACKRAT frames over the air. This has the benefit of continuing to allow Linux userspace tools to work (like cURL, as we’ll try!), which means we don’t have to do a lot of work to implement higher level protocols or tactics to get a connection established over the link.

Given that this post is less RF and more Linuxy, I’m going to include more code snippits than in prior posts, and those snippits are closer to runable Go, but still not complete examples. There’s also a lot of different ways to do this, I’ve just picked the easiest one for me to implement and debug given my existing tooling – for you, you may find another approach easier to implement!

Again, deviation here is very welcome, and since this segment is the least RF centric post in the series, the pace and tone is going to feel different. If you feel lost here, that’s OK. This isn’t the most important part of the series, and is mostly here to give a concrete ending to the story arc. Any way you want to finish your own journy is the best way for you to finish it!

Implement Ethernet conversion code

This assumes an importable package with a Frame struct, which we can use to convert a Frame to/from Ethernet. Given that the PACKRAT frame has a field that Ethernet doesn’t (namely, Callsign), that will need to be explicitly passed in when turning an Ethernet frame into a PACKRAT Frame.

...

// ToPackrat will create a packrat frame from an Ethernet frame.
func ToPackrat(callsign [8]byte, frame *ethernet.Frame) (*packrat.Frame, error) {
        var frameType packrat.FrameType
        switch frame.EtherType {
        case ethernet.EtherTypeIPv4:
                frameType = packrat.FrameTypeIPv4
        default:
                return nil, fmt.Errorf("ethernet: unsupported ethernet type %x", frame.EtherType)
        }

        return &packrat.Frame{
                Destination: frame.Destination,
                Source:      frame.Source,
                Type:        frameType,
                Callsign:    callsign,
                Payload:     frame.Payload,
        }, nil
}

// FromPackrat will create an Ethernet frame from a Packrat frame.
func FromPackrat(frame *packrat.Frame) (*ethernet.Frame, error) {
        var etherType ethernet.EtherType
        switch frame.Type {
        case packrat.FrameTypeRaw:
                return nil, fmt.Errorf("ethernet: unsupported packrat type 'raw'")
        case packrat.FrameTypeIPv4:
                etherType = ethernet.EtherTypeIPv4
        default:
                return nil, fmt.Errorf("ethernet: unknown packrat type %x", frame.Type)
        }

        // We lose the Callsign here, which is sad.
        return &ethernet.Frame{
                Destination: frame.Destination,
                Source:      frame.Source,
                EtherType:   etherType,
                Payload:     frame.Payload,
        }, nil
}

Our helpers, ToPackrat and FromPackrat can now be used to transmorgify PACKRAT into Ethernet, or Ethernet into PACKRAT. Let’s put them into use!

Implement a TAP interface

On Linux, the networking stack can be exposed to userland using TUN or TAP interfaces. TUN devices allow a userspace program to read and write data at the Layer 3 / IP layer. TAP devices allow a userspace program to read and write data at the Layer 2 Data Link / Ethernet layer. Writing data at Layer 2 is what we want to do, since we’re looking to transform our Layer 2 into Ethernet’s Layer 2 Frames. Our first job here is to create the actual TAP interface, set the MAC address, and set the IP range to our pre-coordinated IP range.

...

import (
    "net"

    "github.com/mdlayher/ethernet"
    "github.com/songgao/water"
    "github.com/vishvananda/netlink"
)

...
    config := water.Config{DeviceType: water.TAP}
    config.Name = "rat0"
    iface, err := water.New(config)
    ...
    netIface, err := netlink.LinkByName("rat0")
    ...

    // Pick a range here that works for you!
    //
    // For my local network, I'm using some IPs
    // that AMPR (ampr.org) was nice enough to
    // allocate to me for ham radio use. Thanks,
    // AMPR!
    //
    // Let's just use 10.* here, though.
    //
    ip, cidr, err := net.ParseCIDR("10.0.0.1/24")
    ...
    cidr.IP = ip

    err = netlink.AddrAdd(netIface, &netlink.Addr{
        IPNet: cidr,
        Peer:  cidr,
    })
    ...

    // Add all our neighbors to the ARP table
    for _, neighbor := range neighbors {
        netlink.NeighAdd(&netlink.Neigh{
                LinkIndex:    netIface.Attrs().Index,
                Type:         netlink.FAMILY_V4,
                State:        netlink.NUD_PERMANENT,
                IP:           neighbor.IP,
                HardwareAddr: neighbor.MAC,
        })
    }

    // Pick a MAC that is globally unique here, this is
    // just used as an example!
    addr, err := net.ParseMAC("FA:DE:DC:AB:LE:01")
    ...

    netlink.LinkSetHardwareAddr(netIface, addr)
    ...
    err = netlink.LinkSetUp(netIface)

    var frame = &ethernet.Frame{}
    var buf   = make([]byte, 1500)

    for {
        n, err := iface.Read(buf)
        ...
        err = frame.UnmarshalBinary(buf[:n])
        ...
        // process frame here (to come)
    }
...

Now that our network stack can resolve an IP to a MAC Address (via ip neigh according to our pre-defined neighbors), and send that IP packet to our daemon, it’s now on us to send IPv4 data over the airwaves. Here, we’re going to take packets coming in from our TAP interface, and marshal the Ethernet frame into a PACKRAT Frame and transmit it. As with the rest of the RF code, we’ll leave that up to the implementer, of course, using what was built during Part 2: Transmitting BPSK symbols and Part 4: Framing data.

...
    for {
        // continued from above

        n, err := iface.Read(buf)
        ...
        err = frame.UnmarshalBinary(buf[:n])
        ...

        switch frame.EtherType {
        case 0x0800:
            // ipv4 packet
            pack, err := ToPackrat(
                // Add my callsign to all Frames, for now
                [8]byte{'K', '3', 'X', 'E', 'C'},
                frame,
            )
            ...
            err = transmitPacket(pack)
            ...
        }
    }
...

Now that we have transmitting covered, let’s go ahead and handle the receive path here. We’re going to listen on frequency using the code built in Part 3: Receiving BPSK symbols and Part 4: Framing data. The Frames we decode from the airwaves are expected to come back from the call packratReader.Next in the code below, and the exact way that works is up to the implementer.

...
    for {
        // pull the next packrat frame from
        // the symbol stream as we did in the
        // last post
        packet, err := packratReader.Next()
        ...

        // check for CRC errors and drop invalid
        // packets
        err = packet.Check()
        ...

        if bytes.Equal(packet.Source, addr) {
            // if we've heard ourself transmitting
            // let's avoid looping back
            continue
        }

        // create an ethernet frame
        frame, err := FromPackrat(packet)
        ...

        buf, err := frame.MarshalBinary()
        ...

        // and inject it into the tap
        err = iface.Write(buf)
        ...
    }
...

Phew. Right. Now we should be able to listen for PACKRAT frames on the air and inject them into our TAP interface.

Putting it all Together

After all this work – weeks of work! – we can finally get around to putting some real packets over the air. For me, this was an incredibly satisfying milestone, and tied together months of learning!

I was able to start up a UDP server on a remote machine with an RTL-SDR dongle attached to it, listening on the TAP interface’s host IP with my defined MAC address, and send UDP packets to that server via PACKRAT using my laptop, /dev/udp and an Ettus B210, sending packets into the TAP interface.

Now that UDP was working, I was able to get TCP to work using two PlutoSDRs, which allowed me to run the cURL command I pasted in the first post (both simultaneously listen and transmit on behalf of my TAP interface).

It’s my hope that someone out there will be inspired to implement their own Layer 1 and Layer 2 as a learning exercise, and gets the same sense of gratification that I did! If you’re reading this, and at a point where you’ve been able to send IP traffic over your own Layer 1 / Layer 2, please get in touch! I’d be thrilled to hear all about it. I’d love to link to any posts or examples you publish here!