I am writing a network extension (activated from a MacOS app) which should proxy HTTP connections for certain domains through a custom made proxy server. This is the code I am playing with:
class AppProxyProvider: NETransparentProxyProvider {
static let log = OSLog(subsystem: "com.example.myextension", category: "provider")
override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) {
let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let rule = NENetworkRule(destinationHost: NWHostEndpoint(hostname:"example.com", port:"443"), protocol: .TCP)
settings.includedNetworkRules = [rule]
self.setTunnelNetworkSettings(settings) { error in
completionHandler(error)
}
}
override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: @escaping () -> Void) {
completionHandler()
}
override func wake() {
}
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
os_log(.debug, log: Self.log, "HANDLE NEW FLOW")
return true
}
}
I am able to successfully invoke the startProxy method which in turn registers the custom NETransparentProxyNetworkSettings and invokes the completionHandler without errors. I can see the network extension successfully enabled in the system.
But as soon as I try to make an HTTP request to the specified domain (example.com:443), the request hangs on the client and the handleNewFlow method of my network extension is never called so that I can proxy the traffic.
So my question is what is the correct way to setup a NETransparentProxyProvider (or a NEAppProxyProvider)?
My goal is to intercept connections to specific addresses and proxy them through a local TCP server (by wrapping them in HTTP CONNECT requests)
I also tried subclassing directly from NEAppProxyProvider and registering NETunnelNetworkSettings, but in this case the completion handler of setTunnelNetworkSettings is invoked with an error:
The operation couldn’t be completed. (NEAgentErrorDomain error 1.)
Here's my attempt with NEAppProxyProvider:
class AppProxyProvider: NEAppProxyProvider {
static let log = OSLog(subsystem: "com.example.myextension", category: "provider")
override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) {
let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
self.setTunnelNetworkSettings(settings) { error in
completionHandler(error)
}
}
override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: @escaping () -> Void) {
completionHandler()
}
override func wake() {
}
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
os_log(.debug, log: Self.log, "HANDLE NEW FLOW")
return true
}
}
Post
Replies
Boosts
Views
Activity
I created a NEAppProxyProvider with the following rule:
let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
settings.includedNetworkRules = [
NENetworkRule(destinationHost: NWHostEndpoint(hostname: "example.com", port: "443"), protocol: .TCP)
]
According to the documentation this should match all TCP port 443 traffic to hosts in the "example.com" domain.
But when I test this rule with a client app, I get a "No route to host error" and the handleNewFlow method is not called:
curl https://example.com -v
* Trying 93.184.216.34:443...
* Immediate connect fail for 93.184.216.34: No route to host
* Closing connection 0
curl: (7) Couldn't connect to server
If I use a network rule with a destination network then it works as expected:
settings.includedNetworkRules = [
NENetworkRule(destinationNetwork: NWHostEndpoint(hostname: "93.184.216.34", port: "443"), prefix: 32, protocol: .TCP)
]
Any idea what might be wrong with my domain based rule?
I am developing a MacOS application hosting a Network Extension (app proxy provider). I am signing with Developer ID certificate to distribute outside the AppStore and notarizing the host app with the following entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-proxy-provider-systemextension</string>
</array>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>com.my-organization.my-group</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
After notarizing the app, I am able to install and use the Network Extension.
Now I have a requirement to add the following entitlements (because I need to use some third party native libraries which are signed ad-hoc):
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
As soon as I add those entitlements, the application starts crashing at startup:
Exception Type: EXC_CRASH (SIGKILL (Code Signature Invalid))
Exception Codes: 0x0000000000000000, 0x0000000000000000
Termination Reason: CODESIGNING 1 Taskgated Invalid Signature
Triggered by Thread: 0
Thread 0 Crashed:
0 ??? 0x11cf78ef0 _dyld_start + 0
1 ??? 0x10f62c000 ???
Thread 0 crashed with X86 Thread State (64-bit):
rax: 0x0000000000000000 rbx: 0x0000000000000000 rcx: 0x0000000000000000 rdx: 0x0000000000000000
rdi: 0x0000000000000000 rsi: 0x0000000000000000 rbp: 0x0000000000000000 rsp: 0x00007ff7b08d3b98
r8: 0x0000000000000000 r9: 0x0000000000000000 r10: 0x0000000000000000 r11: 0x0000000000000000
r12: 0x0000000000000000 r13: 0x0000000000000000 r14: 0x0000000000000000 r15: 0x0000000000000000
rip: 0x000000011cf78ef0 rfl: 0x0000000000000200 cr2: 0x0000000000000000
Logical CPU: 0
Error Code: 0x00000000
Trap Number: 0
Binary Images:
0x11cf74000 - 0x11d00bfff ??? (*) <bba77709-6cad-3592-ab03-09d0f7b8610e> ???
0x10f62c000 - 0x10f62dfff ??? (*) <4c4c44aa-5555-3144-a128-fba98974e1e0> ???
Error Formulating Crash Report:
dyld_process_snapshot_get_shared_cache failed
If I remove the com.apple.developer.networking.networkextension and com.apple.developer.system-extension.install, then the app starts but of course I cannot activate and use the Network Extension.
So my question is whether the network extension entitlements and the disable-library-validation entitlements can be used together?
I have a network extension (AppProxyProvider) hosted inside a Mac OS app. Both are signed with the same Developer ID. I am able to programatically install and start the extension from the hosting app:
session.startTunnel()
The proxy provider is successfully started and works as expected.
Unfortunately I am not able to communicate with the extension from the app:
session.sendProviderMessage(data)
Doesn't return any error but the handleAppMessage method is never called. Also I see the following error in the system log:
nesessionmanager: [com.apple.networkextension:] NESMTransparentProxySession[Primary Tunnel:My Transparent Proxy:60F50D75-194D-4FB6-A9D9-7639A561DF5E:(null)]: process 43158 is not entitled to establish IPC with plugins of type xxxxx
where xxxxx is the bundle id of my hosting app.
Is there some entitlement that I am missing? Could this somehow be related with the com.apple.security.application-groups entitlement and the TeamID prefix that gets prepended?
I am testing this on Mac OS Ventura 13.2.1
I have implemented an AppProxyProvider (NETransparentProxyProvider) and I am able to capture traffic with it.
I am also able to define network rules allowing me to exclude some traffic:
let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1:8080")
settings.includedNetworkRules = [
NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "0.0.0.0", port: "0", remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .TCP, direction: .outbound)
]
Now the documentation states that if I want to capture localhost traffic, I need to explicitly add the following rule:
NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "127.0.0.0", port: "0", remotePrefix: 8, localNetwork: nil, localPrefix: 0, protocol: .TCP, direction: .outbound)
and if I want to capture ipv6 localhost address:
NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "::1", port: "0", remotePrefix: 128, localNetwork: nil, localPrefix: 0, protocol: .TCP, direction: .outbound)
All this works great.
Now I am having trouble capturing external ipv6 traffic. For example my ISP supports ipv6 and facebook.com resolves to 2a03:2880:f128:181:face:b00c:0:25de on my machine.
I am unable to write any rule allowing me to capture with the system extension such traffic. Either I get errors that the network mask cannot be greater than 32 or the traffic simply doesn't flow through the extension.
Here's an example request that I would like to capture:
curl https://facebook.com -kvp
* Trying [2a03:2880:f128:181:face:b00c:0:25de]:443...
* Connected to facebook.com (2a03:2880:f128:181:face:b00c:0:25de) port 443 (#0)
* ALPN: offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
* ALPN: server accepted h2
* Server certificate:
* subject: C=US; ST=California; L=Menlo Park; O=Meta Platforms, Inc.; CN=*.facebook.com
* start date: Aug 26 00:00:00 2023 GMT
* expire date: Nov 24 23:59:59 2023 GMT
* issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=DigiCert SHA2 High Assurance Server CA
* SSL certificate verify ok.
* using HTTP/2
* h2 [:method: GET]
* h2 [:scheme: https]
* h2 [:authority: facebook.com]
* h2 [:path: /]
* h2 [user-agent: curl/8.1.2]
* h2 [accept: */*]
* Using Stream ID: 1 (easy handle 0x7fcb5c011e00)
> GET / HTTP/2
> Host: facebook.com
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/2 301
< location: https://www.facebook.com/
< strict-transport-security: max-age=15552000; preload
< content-type: text/html; charset="utf-8"
< x-fb-debug: uWVEw8FZUIXozHae5VgKvIDY5lgH/4Aph+h+nJNJpIr7jFZIFGy9LRLGCSwPudcFBdi4Mf4rLaKsNGCBxHDmrA==
< content-length: 0
< date: Fri, 17 Nov 2023 14:14:03 GMT
< alt-svc: h3=":443"; ma=86400
<
* Connection #0 to host facebook.com left intact
Can this be achieved?