NEFilterPacketContext is Empty

I am implementing a NetworkExtension, based on NEFilterPacketProvider. My issue is that the NEFilterPacketContext is always empty. I assume that's not expected, but my code is as simple as it gets. What's the issue?

Maybe loking at attributeKeys is the wrong thing to do, but in that case, how to access context info? Specifically, I'd like to know things like which applications generated the packet, on which port, what external IP, etc.


class FilterPacketProvider: NEFilterPacketProvider { 
    override init() {
          super.init()
          os_log("FilterPacketProvider init")
      }
 
    override func startFilter(completionHandler: @escaping (Error?) -> Void) {
        os_log("FilterPacketProvider startFilter")
   
        packetHandler = { (context:NEFilterPacketContext,
                           interface:nw_interface_t,
                           direction:NETrafficDirection,
                           packetBytes:UnsafeRawPointer,
                           packetLength:Int)
                                in
                os_log("FilterPacketProvider packet context=%{public}s  %{public}s interface=%{public}s dir=%d length=%d",
                       context.attributeKeys.description,
                       context.debugDescription,
                       interface.description,
                       direction.rawValue,
                       packetLength
                       )
                return .allow //.allow, .drop or .delay
        }
        completionHandler(nil)
    }
 
    override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        os_log("FilterPacketProvider stopFilter")
        completionHandler()
    }
}

Accepted Reply

Do you have a recommendation for a library that would help me in that regard ?

No, sorry. Parsing IP packets is fairly straightforward. In situations where I need to do this, I generally just write the code. Pasted in below is some code I wrote recently as part of some sample code work.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Foundation

struct PacketInfo {

    let addressLength: Int

    let headerRange: Range<Data.Index>
    let headerChecksumRange: Range<Data.Index>?
    let sourceAddressRange: Range<Data.Index>
    let destinationAddressRange: Range<Data.Index>

    let tcpHeaderRange: Range<Data.Index>
    let tcpSourcePortRange: Range<Data.Index>
    let tcpDestinationPortRange: Range<Data.Index>
    let tcpChecksumRange: Range<Data.Index>

    let tcpPayloadRange: Range<Data.Index>

    enum AddressKind {
        case src
        case dst
    }

    func addressRange(kind: AddressKind) -> Range<Data.Index> {
        switch kind {
        case .src: return self.sourceAddressRange
        case .dst: return self.destinationAddressRange
        }
    }

    func portRange(kind: AddressKind) -> Range<Data.Index> {
        switch kind {
        case .src: return self.tcpSourcePortRange
        case .dst: return self.tcpDestinationPortRange
        }
    }

    init?(protocolFamily: sa_family_t, packetData: Data) {
        switch Int32(protocolFamily) {
        case AF_INET:
            try? self.init(packetData4: packetData)
        case AF_INET6:
            // Leaving out IPv6 support for the moment because I’m not set up to
            // test it, and writing all the code without testing it would be a
            // mistake.
            return nil
        default:
            return nil
        }
    }

    private init(packetData4 packetData: Data) throws {
        self.addressLength = 4

        var p = PacketParser(packetData: packetData)

        let versionIHL = try p.parseUInt8()
        try p.skip(1)                       // DSCP | ECN
        let totalLength = try Int(p.parseUInt16())
        try p.skip(2)                       // Identification
        try p.skip(2)                       // Flags | Fragment Offset
        try p.skip(1)                       // TTL
        let proto = try p.parseUInt8()
        self.headerChecksumRange = try p.skip(2)
        self.sourceAddressRange = try p.skip(4)
        self.destinationAddressRange = try p.skip(4)

        guard
            versionIHL & 0xf0 == 0x40,
            versionIHL & 0x0f >= 5,
            totalLength <= packetData.count,
            proto == 6
        else { throw ParseError.unexpectedValue }
        let ipHeaderLength = Int(versionIHL & 0x0f) * 4

        let optionsRange = try p.skip(ipHeaderLength - 20)
        self.headerRange = packetData.startIndex..<optionsRange.upperBound

        self.tcpSourcePortRange = try p.skip(2)
        self.tcpDestinationPortRange = try p.skip(2)
        try p.skip(4)                       // Sequence Number
        try p.skip(4)                       // Acknowledgment Number
        let tcpDataOffsetFlags = try p.parseUInt8()
        try p.skip(1)                       // Flags
        try p.skip(2)                       // Window
        self.tcpChecksumRange = try p.skip(2)
        try p.skip(2)                       // Urgent Pointer

        guard (tcpDataOffsetFlags >> 4) >= 5 else { throw ParseError.unexpectedValue }
        let tcpHeaderLength = Int(tcpDataOffsetFlags >> 4) * 4

        let tcpOptionsRange = try p.skip(tcpHeaderLength - 20)
        self.tcpHeaderRange = optionsRange.upperBound..<tcpOptionsRange.upperBound

        let payloadLength = totalLength - (ipHeaderLength + tcpHeaderLength)
        self.tcpPayloadRange = try p.skip(payloadLength)
    }

    private struct PacketParser {
        let packetData: Data

        init(packetData: Data) {
            self.packetData = packetData
            self.currentIndex = packetData.startIndex
        }

        var currentIndex: Data.Index

        @discardableResult
        mutating func skip(_ count: Int) throws -> Range<Data.Index> {
            let old = self.currentIndex
            guard let newIndex = packetData.index(self.currentIndex, offsetBy: count, limitedBy: packetData.endIndex) else {
                throw ParseError.dataExpected
            }
            defer { self.currentIndex = newIndex }
            return self.currentIndex..<newIndex
        }

        mutating func parseUInt8() throws -> UInt8 {
            let r = try skip(1)
            return self.packetData[r.lowerBound]
        }

        mutating func parseUInt16() throws -> UInt16 {
            let r = try skip(2)
            return UInt16(self.packetData[r.lowerBound]) << 8 | UInt16(self.packetData[r.lowerBound + 1])
        }
    }

    private enum ParseError: Error {
        case dataExpected
        case unexpectedValue
    }
}

Replies

I am seeing the same result and not able to get anything from attributeKeys as well. I am seeing an address description of the object when using debugDescription. Today, the only attributes available for use on NEFilterPacketContext is the attributes available to NSObject.


/**
* @interface NEFilterPacketContext
* @discussion The NEFilterPacketContext class identifies the current filtering context.
*/
@available(OSX 10.15, *)
open class NEFilterPacketContext : NSObject {
}


What are you needing to do with NEFilterPacketContext in your application?


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Hey Matt,


thanks for your answer.


I'd like to know things like which application it came from, where it's going to, etc...

NEFilterPacketContext
is just a token that the system gives to you so that you can, if you like, call
-delayCurrentPacket:
, passing it that context. It has no API; all you can do is pass it around.

Note The reason why you’re seeing the

attributeKeys
method on that object is that it’s declared as a category on
NSObject
in
<Foundation/NSClassDescription.h>
. This is part of Cocoa’s AppleScript support, and is completely meaningless in this context. See this which leads to this.

I'd like to know things like which application it came from, where it's going to, …

Finding out what app it come from is going to be tricky. If you delay the packet (resulting in an

NEPacket
) you may find useful info in the
metadata
property. However, I’d advise against deferring every packet.

To find out where the packet’s going, you should parse the packet’s bytes. It’s a time-honoured tradition (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks Eskimo, that's useful.


Following your advice, I am starting in the grand adventure of parsing packet bytes. I am not very familiar with that. Do you have a recommendation for a library that would help me in that regard ? My current plan is to use the library libcap (pcap). I wouldn't mind something a bit higher level, but it does not seem to be widespread.


Also, if by any chance you have some random code lying around that could get me started on parsing the packet bytes, that would be fantastic.

Thanks

Do you have a recommendation for a library that would help me in that regard ?

No, sorry. Parsing IP packets is fairly straightforward. In situations where I need to do this, I generally just write the code. Pasted in below is some code I wrote recently as part of some sample code work.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Foundation

struct PacketInfo {

    let addressLength: Int

    let headerRange: Range<Data.Index>
    let headerChecksumRange: Range<Data.Index>?
    let sourceAddressRange: Range<Data.Index>
    let destinationAddressRange: Range<Data.Index>

    let tcpHeaderRange: Range<Data.Index>
    let tcpSourcePortRange: Range<Data.Index>
    let tcpDestinationPortRange: Range<Data.Index>
    let tcpChecksumRange: Range<Data.Index>

    let tcpPayloadRange: Range<Data.Index>

    enum AddressKind {
        case src
        case dst
    }

    func addressRange(kind: AddressKind) -> Range<Data.Index> {
        switch kind {
        case .src: return self.sourceAddressRange
        case .dst: return self.destinationAddressRange
        }
    }

    func portRange(kind: AddressKind) -> Range<Data.Index> {
        switch kind {
        case .src: return self.tcpSourcePortRange
        case .dst: return self.tcpDestinationPortRange
        }
    }

    init?(protocolFamily: sa_family_t, packetData: Data) {
        switch Int32(protocolFamily) {
        case AF_INET:
            try? self.init(packetData4: packetData)
        case AF_INET6:
            // Leaving out IPv6 support for the moment because I’m not set up to
            // test it, and writing all the code without testing it would be a
            // mistake.
            return nil
        default:
            return nil
        }
    }

    private init(packetData4 packetData: Data) throws {
        self.addressLength = 4

        var p = PacketParser(packetData: packetData)

        let versionIHL = try p.parseUInt8()
        try p.skip(1)                       // DSCP | ECN
        let totalLength = try Int(p.parseUInt16())
        try p.skip(2)                       // Identification
        try p.skip(2)                       // Flags | Fragment Offset
        try p.skip(1)                       // TTL
        let proto = try p.parseUInt8()
        self.headerChecksumRange = try p.skip(2)
        self.sourceAddressRange = try p.skip(4)
        self.destinationAddressRange = try p.skip(4)

        guard
            versionIHL & 0xf0 == 0x40,
            versionIHL & 0x0f >= 5,
            totalLength <= packetData.count,
            proto == 6
        else { throw ParseError.unexpectedValue }
        let ipHeaderLength = Int(versionIHL & 0x0f) * 4

        let optionsRange = try p.skip(ipHeaderLength - 20)
        self.headerRange = packetData.startIndex..<optionsRange.upperBound

        self.tcpSourcePortRange = try p.skip(2)
        self.tcpDestinationPortRange = try p.skip(2)
        try p.skip(4)                       // Sequence Number
        try p.skip(4)                       // Acknowledgment Number
        let tcpDataOffsetFlags = try p.parseUInt8()
        try p.skip(1)                       // Flags
        try p.skip(2)                       // Window
        self.tcpChecksumRange = try p.skip(2)
        try p.skip(2)                       // Urgent Pointer

        guard (tcpDataOffsetFlags >> 4) >= 5 else { throw ParseError.unexpectedValue }
        let tcpHeaderLength = Int(tcpDataOffsetFlags >> 4) * 4

        let tcpOptionsRange = try p.skip(tcpHeaderLength - 20)
        self.tcpHeaderRange = optionsRange.upperBound..<tcpOptionsRange.upperBound

        let payloadLength = totalLength - (ipHeaderLength + tcpHeaderLength)
        self.tcpPayloadRange = try p.skip(payloadLength)
    }

    private struct PacketParser {
        let packetData: Data

        init(packetData: Data) {
            self.packetData = packetData
            self.currentIndex = packetData.startIndex
        }

        var currentIndex: Data.Index

        @discardableResult
        mutating func skip(_ count: Int) throws -> Range<Data.Index> {
            let old = self.currentIndex
            guard let newIndex = packetData.index(self.currentIndex, offsetBy: count, limitedBy: packetData.endIndex) else {
                throw ParseError.dataExpected
            }
            defer { self.currentIndex = newIndex }
            return self.currentIndex..<newIndex
        }

        mutating func parseUInt8() throws -> UInt8 {
            let r = try skip(1)
            return self.packetData[r.lowerBound]
        }

        mutating func parseUInt16() throws -> UInt16 {
            let r = try skip(2)
            return UInt16(self.packetData[r.lowerBound]) << 8 | UInt16(self.packetData[r.lowerBound + 1])
        }
    }

    private enum ParseError: Error {
        case dataExpected
        case unexpectedValue
    }
}

Thanks, very useful!

Oh, one thing I should mention. This code parses IP packets — because it come from a project that’s not a packet filter — but the packets passed to your packet filter include the link-layer header. You’ll have to adapt the code to parse past that header.

Keep in mind that the link-layer header could be different between link layers. Use the

interface
parameter to your filter to determine what type of interface you’re dealing with.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Ok, thanks for that. I will start by parsing the link-layer header then. You don't happen to also have some code doing this by any chance, to get me started?

No, but the link-layer header for Ethernet-like interfaces is very simple.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Yes, you're right, very straightfoward. Thanks a lot!

Hey,


I'm having trouble using the context in my app as well.


I'm trying to delay a packet with `delayCurrentPacket` but Xcode is having trouble with this compilation, acting as if the context in my closure does not exist. Here's my code:


    private var _packetHandler: NEFilterPacketHandler? =
    {
        context, interface, direction, packetBytes, packetLength in
        
        let data = Data( bytes: packetBytes, count: packetLength )
        var hostname: String
        var `protocol` = Protocol.ssl
        
        // Assume it's HTTPS
        //
        hostname = packetUtilWrapper.getHostForPossibleHTTPSRequest( data )
        if hostname.isEmpty // Maybe HTTP?
        {
            hostname = packetUtilWrapper.getHostForPossibleHTTPRequest( data )
            `protocol` = .http
        }
        
        // Don't spam logs with anything but HTTP/HTTPS
        //
        if !hostname.isEmpty
        {
            log( (`protocol`, hostname) )
            let packet = delayCurrentPacket( context )
            return .delay
        }

        return .allow
    }



The error is: 'NEFilterPacketContext' is not convertible to 'FilterPacketProvider'

Additionally, the NEPacket metadata property is nil...any documentation on metadata?

I'm trying to delay a packet with

delayCurrentPacket(_:)
but Xcode is having trouble with this compilation, acting as if the context in my closure does not exist.

It’s hard to say for sure without more context but I believe this problem is caused by you trying to put the packet handler into a property. You can’t use

self
is the initial value for a property closure because it represents an initialisation check’n’egg problem (
self
isn’t valid until the initialisation isn’t complete, but the initialisation can’t complete without
self
being valid).

I copied your code into the

startFilter(completionHandler:)
method that comes as part of the template and it compiles just fine (once I commented out all of the stuff specific to your project).

Additionally, the

NEPacket
metadata
property is nil...any documentation on
metadata
?

There’s a fine doc comment in

<NetworkExtension/NEPacket.h>
. In short, this is typically only valid in per-app VPN scenarios.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you Quinn!


Yes; that was a simple mistake on my part...


Ah ok, metadata is probably expected to be nil for us, then.


Now that the former is working, I'm having trobule with delayCurrentPacket.


The docs indicate we should return a .delay verdict after calling delayCurrentPacket. Once delayed, we can call allow( <packet> ) with the previousy delayed packet or choose to drop the packet by releasing it.


What does "releasing" mean in this context? Because we've already returned the .delay verdict, we cannot subsequently return a followup verdict of .drop from our handler any longer because it has already returned. It seems we should have a function called drop(<packet>) similarly to our allow(<packet>) function, where we can drop the packet after we've returned the .delay verdict.


While it is possible we can simply never call allow(<packet>) and the connection may eventually time out and drop on its own, this is not ideal for a great UX.


Is it possible my async nature on this code is incorrect? Should I instead be making these decisions sync to avoid the need to use delay at all?


I'm worried that if the blocking operation is holding up other packets waiting to enter the handler, we may slow other traffic until the completion of the blocking operation before it. Though if the extension is running each packet through the packet handler on its own private thread/queue, this blocking may not be a problem.


Do you have suggestions on either of these options?


    override func startFilter(completionHandler: @escaping (Error?) -> Void) {
        os_log( #function )
        
        packetHandler =
        {
            context, interface, direction, packetBytes, packetLength in
            guard direction == .outbound else { return .allow }
            
            let data = Data( bytes: packetBytes, count: packetLength )
            var hostname: String
            var `protocol` = Protocol.ssl
            
            // Assume it's HTTPS
            //
            hostname = packetUtilWrapper.getHostForPossibleHTTPSRequest( data )
            if hostname.isEmpty // Maybe HTTP?
            {
                hostname = packetUtilWrapper.getHostForPossibleHTTPRequest( data )
                `protocol` = .http
            }
            
            // Don't spam logs with anything but HTTP/HTTPS
            //
            if !hostname.isEmpty
            {
                let packet = self.delayCurrentPacket( context )
                
                let urlString = `protocol`.rawValue.appending( "://" ).appending( hostname )
                let postData = Data( "{\"url\":\"\( urlString )\"}".utf8 )
                var request = URLRequest( url: URL( string: "http://myLocalAPI.json")! )
                request.httpMethod = "POST"
                request.httpBody = postData
                
                FilterPacketProvider.queue.async
                {
                    URLSession.shared.dataTask( with: request )
                    {
                        data, response, error in
                        guard let data = data,
                            let json = try? JSONSerialization.jsonObject( with: data, options: [] ) as? [String: Any] else
                        {
                            self.allow( packet )
                            FilterPacketProvider.log( (`protocol`, "Bad response: ".appending( hostname ) ) )
                            return
                        }
                        
                        let results = json["result"] as! [String: Any]
                        let records = results["records"] as! [String: Any]
                        let action = records["action"] as! String
                        if action == "allow"
                        {
                            self.allow( packet )
                        }
                        else
                        {
                            // TODO: How we do drop the packet now?
                            //
                        }
                        
                        FilterPacketProvider.log( (`protocol`, "\( action ): ".appending( hostname ) ) )
                        
                    }.resume()
                }

                
                return .delay
            }
            
            return .allow
        }


Thanks again!

What does "releasing" mean in this context?

It’s using old school Cocoa terms, from before the advent of ARC, where you would have to manually retain and release objects. If you want to allow a packet, you must hold on to a reference to it and then eventually call

allow
. OTOH, if you want to drop the packet, just release your reference and you’re done.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"