NEFilterPacketProvider and UDP

I must be missing something obvious here: I've got my packet filter running (yay), but every UDP packet it gets has a destination port of 0. Also I am confused by this other behaviour:

            let udpHeader = (bytes + etherHeaderSize + ip4HeaderSize).bindMemory(to: udphdr.self, capacity: udpHeaderSize)
            switch Int(udpHeader.pointee.uh_dport).bigEndian {
            case 80, 443:
                return true
            case 0:
                os_log(.debug, log: Self.log, "UDP port 0: ip_dst = %{public}s", ReadableIPAddr(ipPacket.pointee.ip_dst))
                return false
            default:
                os_log(.debug, log: Self.log, "Got UDP packet dest port %#x, ip_dst = %{public}s", Int(udpHeader.pointee.uh_dport).bigEndian, ReadableIPAddr(ipPacket.pointee.ip_dst))
              return false
            }

The case 0 is not used, even though the default prints out a value of 0.

Answered by DTS Engineer in 717285022

This is working for me:

let packet: [UInt8] = [
    0x2c, 0x91, 0xab, 0x54, 0x3c, 0xbe, 0x3c, 0x22,
    0xfb, 0x0c, 0x7b, 0x27, 0x08, 0x00, 0x45, 0x00,
    0x00, 0x39, 0x07, 0x3a, 0x00, 0x00, 0x40, 0x11,
    0xaf, 0x89, 0xc0, 0xa8, 0x01, 0x47, 0x01, 0x01,
    0x01, 0x01, 0xfc, 0x0f, 0x00, 0x35, 0x00, 0x25,
    0x8c, 0xf0, 0xe3, 0x0f, 0x01, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65,
    0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63,
    0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01,
]
let etherHeaderSize = 14
let ip4HeaderSize = 20
packet.withUnsafeBytes { buf in
    let bytes = buf.baseAddress!
    let udpHeader = (bytes + etherHeaderSize + ip4HeaderSize).bindMemory(to: udphdr.self, capacity: 1)
    print(UInt16(bigEndian: udpHeader.pointee.uh_dport))
}

It prints 53, as expected.

In creating this test I saw two problems with your code:

  • You pass udpHeaderSize to the capacity parameter, not 1. That parameter is meant to be the number of items at that location, not the size of the item in bytes. IMO this is unlikely to be the cause of your problem.

  • You had Int(udpHeader.pointee.uh_dport).bigEndian, which is backwards. You want UInt16(bigEndian: udpHeader.pointee.uh_dport).

Having said that, I generally discourage folks from doing this sort of pointer manipulation, in Swift specifically but also in C. It’s very easy to break the rules and end up relying on undefined behaviour.

IMO you’re much better off parsing packets byte-by-byte. That might be too slow in a packet filter provider, but I recommend that you start by assuming it’s not too slow and then only mess around with pointers once you’ve proved that you have a performance problem.

Pasted in below is an example of how I’d approach this. You’d be amazed at how efficient this can be once you let the optimiser loose on it (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


extension Collection where Self.SubSequence == Self {

    mutating func consume(count: Int) -> Self.SubSequence? {
        let result = self.prefix(count)
        guard result.count == count else { return nil }
        self = self.dropFirst(count)
        return result
    }
}

extension Collection where Self.SubSequence == Self, Self.Element == UInt8 {

    mutating func consume<T>(bigEndian _: T.Type) -> T? where T: UnsignedInteger {
        guard let bytes = self.consume(count: MemoryLayout<T>.size) else { return nil }
        return bytes.reduce(0, { $0 << 8 | T($1) } )
    }
}

packet.withUnsafeBytes { buf in
    let etherHeaderSize = 14
    let ip4HeaderSize = 20
    var remainder = buf[...]

    guard let ethernetHeader = remainder.consume(count: etherHeaderSize) else { fatalError() }
    guard let ipHeader = remainder.consume(count: ip4HeaderSize) else { fatalError() }
    guard
        let srcPort = remainder.consume(bigEndian: UInt16.self),
        let dstPort = remainder.consume(bigEndian: UInt16.self),
        let length = remainder.consume(bigEndian: UInt16.self),
        let checksum = remainder.consume(bigEndian: UInt16.self)
    else { fatalError() }
    
    print(srcPort)      // 64527
    print(dstPort)      // 53
    print(length)       // 37
    print(checksum)     // 36080
}

So clearly the pointer is wrong. I'm not sure why it's wrong, though. But looking at the packet (bytes + etherHeaderSize), I find the IPv4 header as expected, and then the UDP header as expected.

But looking at the packet (bytes + etherHeaderSize), I find the IPv4 header as expected, and then the UDP header as expected.

I’m not sure how to interpret that. My best guess is that:

  • Looking at bytes in the debugger, you see it laid out correctly, that is, an Ethernet header followed by an IP header followed by a UDP header.

  • However, your Swift code to get the destination port is returning the wrong value.

Is that what you’re saying? If so:

  • What type is bytes?

  • What are the values of etherHeaderSize and ip4HeaderSize?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

bytes is the UnsafeRawPointer the NEFilterPacketProvider handler gets from the system. The values of etherHeaderSize and ip4HeaderSize are the expected 14 and 20, respectively.

I feel like I am missing something very obvious here.

I rewrote the packet examination code in ObjC, and it works as expected.

I still have no idea what's going on with the Swift code -- the fact that case 0: didn't get caught, but the default: case showed it with a value of 0 just doesn't make any sense to me. 😩

Accepted Answer

This is working for me:

let packet: [UInt8] = [
    0x2c, 0x91, 0xab, 0x54, 0x3c, 0xbe, 0x3c, 0x22,
    0xfb, 0x0c, 0x7b, 0x27, 0x08, 0x00, 0x45, 0x00,
    0x00, 0x39, 0x07, 0x3a, 0x00, 0x00, 0x40, 0x11,
    0xaf, 0x89, 0xc0, 0xa8, 0x01, 0x47, 0x01, 0x01,
    0x01, 0x01, 0xfc, 0x0f, 0x00, 0x35, 0x00, 0x25,
    0x8c, 0xf0, 0xe3, 0x0f, 0x01, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65,
    0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63,
    0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01,
]
let etherHeaderSize = 14
let ip4HeaderSize = 20
packet.withUnsafeBytes { buf in
    let bytes = buf.baseAddress!
    let udpHeader = (bytes + etherHeaderSize + ip4HeaderSize).bindMemory(to: udphdr.self, capacity: 1)
    print(UInt16(bigEndian: udpHeader.pointee.uh_dport))
}

It prints 53, as expected.

In creating this test I saw two problems with your code:

  • You pass udpHeaderSize to the capacity parameter, not 1. That parameter is meant to be the number of items at that location, not the size of the item in bytes. IMO this is unlikely to be the cause of your problem.

  • You had Int(udpHeader.pointee.uh_dport).bigEndian, which is backwards. You want UInt16(bigEndian: udpHeader.pointee.uh_dport).

Having said that, I generally discourage folks from doing this sort of pointer manipulation, in Swift specifically but also in C. It’s very easy to break the rules and end up relying on undefined behaviour.

IMO you’re much better off parsing packets byte-by-byte. That might be too slow in a packet filter provider, but I recommend that you start by assuming it’s not too slow and then only mess around with pointers once you’ve proved that you have a performance problem.

Pasted in below is an example of how I’d approach this. You’d be amazed at how efficient this can be once you let the optimiser loose on it (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


extension Collection where Self.SubSequence == Self {

    mutating func consume(count: Int) -> Self.SubSequence? {
        let result = self.prefix(count)
        guard result.count == count else { return nil }
        self = self.dropFirst(count)
        return result
    }
}

extension Collection where Self.SubSequence == Self, Self.Element == UInt8 {

    mutating func consume<T>(bigEndian _: T.Type) -> T? where T: UnsignedInteger {
        guard let bytes = self.consume(count: MemoryLayout<T>.size) else { return nil }
        return bytes.reduce(0, { $0 << 8 | T($1) } )
    }
}

packet.withUnsafeBytes { buf in
    let etherHeaderSize = 14
    let ip4HeaderSize = 20
    var remainder = buf[...]

    guard let ethernetHeader = remainder.consume(count: etherHeaderSize) else { fatalError() }
    guard let ipHeader = remainder.consume(count: ip4HeaderSize) else { fatalError() }
    guard
        let srcPort = remainder.consume(bigEndian: UInt16.self),
        let dstPort = remainder.consume(bigEndian: UInt16.self),
        let length = remainder.consume(bigEndian: UInt16.self),
        let checksum = remainder.consume(bigEndian: UInt16.self)
    else { fatalError() }
    
    print(srcPort)      // 64527
    print(dstPort)      // 53
    print(length)       // 37
    print(checksum)     // 36080
}

Oh oh oh oh. I think I see my problem, then: Int(nework_byte_order_short).bigEndian may be what's biting me bigly! Oooh.

I hadn't realized that the capacity was number of designated types, not byte length. Although now I'm surprised it didn't crash there. 😄

I do like your Swiftian consumption model a lot. However, I do think it's okay for me to stick with pointers in C, given my background.

At this point, I think I'm going to stick with the hybrid model (that is, it calls an ObjC class), but I am copying that Swift code and putting it into my Notes.

NEFilterPacketProvider and UDP
 
 
Q