Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
423 changes: 423 additions & 0 deletions swift-extension/GreywallProxy.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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>SchemeUserState</key>
<dict>
<key>GreywallProxy.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>GreywallProxyExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
</dict>
</dict>
</plist>
102 changes: 102 additions & 0 deletions swift-extension/GreywallProxy/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Cocoa
import SystemExtensions
import NetworkExtension
import os.log

@main
class AppDelegate: NSObject, NSApplicationDelegate, OSSystemExtensionRequestDelegate {

private let log = Logger(subsystem: "io.greywall.proxy.app", category: "app")
private let extensionBundleID = "io.greywall.proxy.extension"

// MARK: - App lifecycle

func applicationDidFinishLaunching(_ notification: Notification) {
log.info("GreywallProxy app launched")
activateExtension()
}

// MARK: - System extension activation

private func activateExtension() {
log.info("Requesting activation of \(self.extensionBundleID)")
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: extensionBundleID,
queue: .main
)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)
}

// MARK: - OSSystemExtensionRequestDelegate

func request(_ request: OSSystemExtensionRequest,
didFinishWithResult result: OSSystemExtensionRequest.Result) {
log.info("Extension activation finished: \(result.rawValue)")
switch result {
case .completed:
log.info("Extension activated, configuring proxy manager")
configureProxyManager()
case .willCompleteAfterReboot:
log.info("Extension will activate after reboot")
@unknown default:
log.warning("Unknown result: \(result.rawValue)")
}
}

func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
log.error("Extension activation failed: \(error.localizedDescription)")
}

func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
log.info("User approval needed -- check System Settings > Privacy & Security")
}

func request(_ request: OSSystemExtensionRequest,
actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
log.info("Replacing existing extension v\(existing.bundleShortVersion) with v\(ext.bundleShortVersion)")
return .replace
}

// MARK: - Proxy manager configuration

private func configureProxyManager() {
NETransparentProxyManager.loadAllFromPreferences { managers, error in
if let error {
self.log.error("Failed to load proxy managers: \(error.localizedDescription)")
return
}

let manager = managers?.first ?? NETransparentProxyManager()

let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = self.extensionBundleID
proto.serverAddress = "127.0.0.1"

manager.protocolConfiguration = proto
manager.localizedDescription = "Greywall Proxy"
manager.isEnabled = true

manager.saveToPreferences { error in
if let error {
self.log.error("Failed to save proxy config: \(error.localizedDescription)")
return
}
self.log.info("Proxy config saved, starting tunnel")
manager.loadFromPreferences { error in
if let error {
self.log.error("Failed to reload: \(error.localizedDescription)")
return
}
do {
try manager.connection.startVPNTunnel()
self.log.info("Tunnel started")
} catch {
self.log.error("Failed to start tunnel: \(error.localizedDescription)")
}
}
}
}
}
}
12 changes: 12 additions & 0 deletions swift-extension/GreywallProxy/GreywallProxy.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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.system-extension.install</key>
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-proxy-provider-systemextension</string>
</array>
</dict>
</plist>
26 changes: 26 additions & 0 deletions swift-extension/GreywallProxy/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSUIElement</key>
<true/>
<key>NSSystemExtensionUsageDescription</key>
<string>Greywall needs a system extension to transparently intercept network traffic from sandboxed processes.</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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>
</dict>
</plist>
27 changes: 27 additions & 0 deletions swift-extension/GreywallProxyExtension/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.app-proxy</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).TransparentProxyProvider</string>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import NetworkExtension
import os.log

class TransparentProxyProvider: NETransparentProxyProvider {

private let log = Logger(subsystem: "io.greywall.proxy", category: "provider")

// MARK: - Lifecycle

override func startProxy(options: [String: Any]?, completionHandler: @escaping (Error?) -> Void) {
log.info("startProxy called")

let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1")

// Capture all outbound TCP
let tcpRule = NENetworkRule(
remoteNetwork: nil, remotePrefix: 0,
localNetwork: nil, localPrefix: 0,
protocol: .TCP, direction: .outbound
)

// Capture all outbound UDP (includes DNS on port 53)
let udpRule = NENetworkRule(
remoteNetwork: nil, remotePrefix: 0,
localNetwork: nil, localPrefix: 0,
protocol: .UDP, direction: .outbound
)

// Exclude loopback to avoid interfering with local services
let loopbackV4 = NENetworkRule(
remoteNetwork: NWHostEndpoint(hostname: "127.0.0.0", port: "0"),
remotePrefix: 8,
localNetwork: nil, localPrefix: 0,
protocol: .any, direction: .any
)
let loopbackV6 = NENetworkRule(
remoteNetwork: NWHostEndpoint(hostname: "::1", port: "0"),
remotePrefix: 128,
localNetwork: nil, localPrefix: 0,
protocol: .any, direction: .any
)

settings.includedNetworkRules = [tcpRule, udpRule]
settings.excludedNetworkRules = [loopbackV4, loopbackV6]

setTunnelNetworkSettings(settings) { error in
if let error {
self.log.error("Failed to set network settings: \(error.localizedDescription)")
} else {
self.log.info("Network settings applied, proxy active")
}
completionHandler(error)
}
}

override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
log.info("stopProxy called, reason: \(String(describing: reason))")
completionHandler()
}

// MARK: - Flow handling (Step 1: passive logging, all passthrough)

override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
let meta = flow.metaData
let signingID = meta.sourceAppSigningIdentifier
let hostname = flow.remoteHostname ?? "<no hostname>"
let pid = extractPID(from: meta.sourceAppAuditToken)

if let tcpFlow = flow as? NEAppProxyTCPFlow {
let endpoint = tcpFlow.remoteEndpoint as? NWHostEndpoint
let dest = endpoint.map { "\($0.hostname):\($0.port)" } ?? "<unknown>"
log.info("TCP flow: pid=\(pid) app=\(signingID) host=\(hostname) dest=\(dest)")
} else if flow is NEAppProxyUDPFlow {
log.info("UDP flow: pid=\(pid) app=\(signingID) host=\(hostname)")
}

// Step 1: passthrough everything -- just log and let it through
return false
}

// MARK: - Helpers

private func extractPID(from auditToken: Data?) -> pid_t {
guard let token = auditToken, token.count >= 24 else { return -1 }
// audit_token_t is 8 x UInt32; PID is at index 5 (byte offset 20)
return token.withUnsafeBytes { ptr in
let tokens = ptr.bindMemory(to: UInt32.self)
return pid_t(tokens[5])
}
}
}
9 changes: 9 additions & 0 deletions swift-extension/GreywallProxyExtension/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation
import NetworkExtension
import os.log

let log = Logger(subsystem: "io.greywall.proxy", category: "extension")
log.info("GreywallProxy network extension starting")

NEProvider.startSystemExtensionMode()
dispatchMain()
Loading
Loading