Skip to content

Commit d590bfb

Browse files
committed
Merge branch 'josezy-macos-traffic-capture'
2 parents 9b7bfd8 + 066e0bb commit d590bfb

File tree

13 files changed

+932
-0
lines changed

13 files changed

+932
-0
lines changed

swift-extension/GreywallProxy.xcodeproj/project.pbxproj

Lines changed: 423 additions & 0 deletions
Large diffs are not rendered by default.

swift-extension/GreywallProxy.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>GreywallProxy.xcscheme_^#shared#^_</key>
8+
<dict>
9+
<key>orderHint</key>
10+
<integer>0</integer>
11+
</dict>
12+
<key>GreywallProxyExtension.xcscheme_^#shared#^_</key>
13+
<dict>
14+
<key>orderHint</key>
15+
<integer>1</integer>
16+
</dict>
17+
</dict>
18+
</dict>
19+
</plist>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import Cocoa
2+
import SystemExtensions
3+
import NetworkExtension
4+
import os.log
5+
6+
@main
7+
class AppDelegate: NSObject, NSApplicationDelegate, OSSystemExtensionRequestDelegate {
8+
9+
private let log = Logger(subsystem: "io.greywall.proxy.app", category: "app")
10+
private let extensionBundleID = "io.greywall.proxy.extension"
11+
12+
// MARK: - App lifecycle
13+
14+
func applicationDidFinishLaunching(_ notification: Notification) {
15+
log.info("GreywallProxy app launched")
16+
activateExtension()
17+
}
18+
19+
// MARK: - System extension activation
20+
21+
private func activateExtension() {
22+
log.info("Requesting activation of \(self.extensionBundleID)")
23+
let request = OSSystemExtensionRequest.activationRequest(
24+
forExtensionWithIdentifier: extensionBundleID,
25+
queue: .main
26+
)
27+
request.delegate = self
28+
OSSystemExtensionManager.shared.submitRequest(request)
29+
}
30+
31+
// MARK: - OSSystemExtensionRequestDelegate
32+
33+
func request(_ request: OSSystemExtensionRequest,
34+
didFinishWithResult result: OSSystemExtensionRequest.Result) {
35+
log.info("Extension activation finished: \(result.rawValue)")
36+
switch result {
37+
case .completed:
38+
log.info("Extension activated, configuring proxy manager")
39+
configureProxyManager()
40+
case .willCompleteAfterReboot:
41+
log.info("Extension will activate after reboot")
42+
@unknown default:
43+
log.warning("Unknown result: \(result.rawValue)")
44+
}
45+
}
46+
47+
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
48+
log.error("Extension activation failed: \(error.localizedDescription)")
49+
}
50+
51+
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
52+
log.info("User approval needed -- check System Settings > Privacy & Security")
53+
}
54+
55+
func request(_ request: OSSystemExtensionRequest,
56+
actionForReplacingExtension existing: OSSystemExtensionProperties,
57+
withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
58+
log.info("Replacing existing extension v\(existing.bundleShortVersion) with v\(ext.bundleShortVersion)")
59+
return .replace
60+
}
61+
62+
// MARK: - Proxy manager configuration
63+
64+
private func configureProxyManager() {
65+
NETransparentProxyManager.loadAllFromPreferences { managers, error in
66+
if let error {
67+
self.log.error("Failed to load proxy managers: \(error.localizedDescription)")
68+
return
69+
}
70+
71+
let manager = managers?.first ?? NETransparentProxyManager()
72+
73+
let proto = NETunnelProviderProtocol()
74+
proto.providerBundleIdentifier = self.extensionBundleID
75+
proto.serverAddress = "127.0.0.1"
76+
77+
manager.protocolConfiguration = proto
78+
manager.localizedDescription = "Greywall Proxy"
79+
manager.isEnabled = true
80+
81+
manager.saveToPreferences { error in
82+
if let error {
83+
self.log.error("Failed to save proxy config: \(error.localizedDescription)")
84+
return
85+
}
86+
self.log.info("Proxy config saved, starting tunnel")
87+
manager.loadFromPreferences { error in
88+
if let error {
89+
self.log.error("Failed to reload: \(error.localizedDescription)")
90+
return
91+
}
92+
do {
93+
try manager.connection.startVPNTunnel()
94+
self.log.info("Tunnel started")
95+
} catch {
96+
self.log.error("Failed to start tunnel: \(error.localizedDescription)")
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.developer.system-extension.install</key>
6+
<true/>
7+
<key>com.apple.developer.networking.networkextension</key>
8+
<array>
9+
<string>app-proxy-provider-systemextension</string>
10+
</array>
11+
</dict>
12+
</plist>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundlePackageType</key>
16+
<string>APPL</string>
17+
<key>CFBundleShortVersionString</key>
18+
<string>1.0</string>
19+
<key>CFBundleVersion</key>
20+
<string>1</string>
21+
<key>LSUIElement</key>
22+
<true/>
23+
<key>NSSystemExtensionUsageDescription</key>
24+
<string>Greywall needs a system extension to transparently intercept network traffic from sandboxed processes.</string>
25+
</dict>
26+
</plist>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.developer.networking.networkextension</key>
6+
<array>
7+
<string>app-proxy-provider-systemextension</string>
8+
</array>
9+
</dict>
10+
</plist>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundleShortVersionString</key>
16+
<string>1.0</string>
17+
<key>CFBundleVersion</key>
18+
<string>1</string>
19+
<key>NSExtension</key>
20+
<dict>
21+
<key>NSExtensionPointIdentifier</key>
22+
<string>com.apple.networkextension.app-proxy</string>
23+
<key>NSExtensionPrincipalClass</key>
24+
<string>$(PRODUCT_MODULE_NAME).TransparentProxyProvider</string>
25+
</dict>
26+
</dict>
27+
</plist>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import NetworkExtension
2+
import os.log
3+
4+
class TransparentProxyProvider: NETransparentProxyProvider {
5+
6+
private let log = Logger(subsystem: "io.greywall.proxy", category: "provider")
7+
8+
// MARK: - Lifecycle
9+
10+
override func startProxy(options: [String: Any]?, completionHandler: @escaping (Error?) -> Void) {
11+
log.info("startProxy called")
12+
13+
let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
14+
15+
// Capture all outbound TCP
16+
let tcpRule = NENetworkRule(
17+
remoteNetwork: nil, remotePrefix: 0,
18+
localNetwork: nil, localPrefix: 0,
19+
protocol: .TCP, direction: .outbound
20+
)
21+
22+
// Capture all outbound UDP (includes DNS on port 53)
23+
let udpRule = NENetworkRule(
24+
remoteNetwork: nil, remotePrefix: 0,
25+
localNetwork: nil, localPrefix: 0,
26+
protocol: .UDP, direction: .outbound
27+
)
28+
29+
// Exclude loopback to avoid interfering with local services
30+
let loopbackV4 = NENetworkRule(
31+
remoteNetwork: NWHostEndpoint(hostname: "127.0.0.0", port: "0"),
32+
remotePrefix: 8,
33+
localNetwork: nil, localPrefix: 0,
34+
protocol: .any, direction: .any
35+
)
36+
let loopbackV6 = NENetworkRule(
37+
remoteNetwork: NWHostEndpoint(hostname: "::1", port: "0"),
38+
remotePrefix: 128,
39+
localNetwork: nil, localPrefix: 0,
40+
protocol: .any, direction: .any
41+
)
42+
43+
settings.includedNetworkRules = [tcpRule, udpRule]
44+
settings.excludedNetworkRules = [loopbackV4, loopbackV6]
45+
46+
setTunnelNetworkSettings(settings) { error in
47+
if let error {
48+
self.log.error("Failed to set network settings: \(error.localizedDescription)")
49+
} else {
50+
self.log.info("Network settings applied, proxy active")
51+
}
52+
completionHandler(error)
53+
}
54+
}
55+
56+
override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
57+
log.info("stopProxy called, reason: \(String(describing: reason))")
58+
completionHandler()
59+
}
60+
61+
// MARK: - Flow handling (Step 1: passive logging, all passthrough)
62+
63+
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
64+
let meta = flow.metaData
65+
let signingID = meta.sourceAppSigningIdentifier
66+
let hostname = flow.remoteHostname ?? "<no hostname>"
67+
let pid = extractPID(from: meta.sourceAppAuditToken)
68+
69+
if let tcpFlow = flow as? NEAppProxyTCPFlow {
70+
let endpoint = tcpFlow.remoteEndpoint as? NWHostEndpoint
71+
let dest = endpoint.map { "\($0.hostname):\($0.port)" } ?? "<unknown>"
72+
log.info("TCP flow: pid=\(pid) app=\(signingID) host=\(hostname) dest=\(dest)")
73+
} else if flow is NEAppProxyUDPFlow {
74+
log.info("UDP flow: pid=\(pid) app=\(signingID) host=\(hostname)")
75+
}
76+
77+
// Step 1: passthrough everything -- just log and let it through
78+
return false
79+
}
80+
81+
// MARK: - Helpers
82+
83+
private func extractPID(from auditToken: Data?) -> pid_t {
84+
guard let token = auditToken, token.count >= 24 else { return -1 }
85+
// audit_token_t is 8 x UInt32; PID is at index 5 (byte offset 20)
86+
return token.withUnsafeBytes { ptr in
87+
let tokens = ptr.bindMemory(to: UInt32.self)
88+
return pid_t(tokens[5])
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)