Packet Bridge is a SwiftUI app with an NEPacketTunnelProvider extension. It exposes two independent on-device interfaces:
- A loopback SOCKS5 server that forwards TCP and UDP traffic to the internet.
- A bidirectional UDP bridge that exchanges raw IP packets with another local process.
The project is intentionally dependency-free and can be edited on Linux. Xcode and an Apple Developer signing identity are required only when it is built and installed on iOS.
The target layout and tunnel lifecycle follow the same useful pattern as LocalDevVPN, while the packet relay and SOCKS5 implementation here are new.
Platform limitation: Apple documents hosting a listener/proxy inside a packet-tunnel provider as an unsupported packet-tunnel use case (TN3120). This project is suitable for controlled development or sideloaded experiments, but that part of the design can encounter OS edge cases and is not a safe assumption for App Store approval. A conventional remote VPN transport is the supported production model.
local proxy app
├─ TCP/UDP SOCKS5 ──> 127.0.0.1:11080 ──> internet
│ PacketTunnelProvider sockets
│
└─ raw IP socket bound to 127.0.0.1:9090
▲ │
│ returned raw IP │ captured raw IP
│ to 127.0.0.1:9091 ▼
└──────── connected UDP bridge <── packetFlow
extension binds :9091
Default ports are configurable in the UI:
| Setting | Default | Meaning |
|---|---|---|
| SOCKS5 listen port | 11080 |
TCP and UDP SOCKS5 endpoint on 127.0.0.1 |
| Maximum SOCKS5 TCP clients | 1024 |
Concurrent TCP clients accepted by the SOCKS5 server; configurable from 128 to 65536 |
| Bridge peer port | 9090 |
Port bound by the local proxy/helper |
| Bridge extension port | 9091 |
Source/listen port bound by the tunnel extension |
| DNS servers | 1.1.1.1, 1.0.0.1 |
Resolver addresses advertised by the packet tunnel |
All listeners are restricted to IPv4 loopback. No LAN-facing proxy is created.
The packet tunnel advertises 1.1.1.1 and 1.0.0.1 as full-tunnel DNS
resolvers. Their DNS packets pass through the raw-IP bridge and follow the same
V2Ray routing policy as other tunnel traffic.
- SOCKS5 version 5 with the
NO AUTHENTICATION REQUIREDmethod. - TCP
CONNECT. - UDP
ASSOCIATE, using UDP on the same numeric port as the TCP listener. - IPv4, IPv6, and domain-name destinations.
- A configurable concurrent TCP client limit, defaulting to 1024. Higher limits consume more packet-tunnel extension memory.
- The TCP control connection must remain open for the lifetime of a UDP association.
- UDP
FRAGvalues other than zero are dropped; fragmentation/reassembly is not implemented. BINDand username/password authentication are not implemented.
The relay's outgoing NWConnection objects are created by the packet-tunnel provider. Provider-originated connections use the provider's underlying network path and are not injected back into its own packet flow.
The local peer binds a UDP socket to 127.0.0.1:<bridge-peer-port>. The extension binds 127.0.0.1:<bridge-extension-port> and connects that socket to the peer.
- Extension to peer: one UDP datagram contains exactly one complete raw IPv4 or IPv6 packet read from
NEPacketTunnelFlow. - Peer to extension: send one complete raw IPv4 or IPv6 packet per datagram, from the peer's bound socket to the extension port.
- There is no extra length, protocol, or channel header. The extension identifies IPv4/IPv6 from the high nibble of the first byte.
- The configured MTU defaults to 1500, so a bridged packet remains well below the UDP datagram size limit.
- Packets with an invalid IP version are dropped.
The same peer socket should be used for receive and send. Because the extension uses connected UDP, datagrams from unrelated source ports are not accepted.
Route all traffic installs IPv4 and, optionally, IPv6 default routes into the packet tunnel. The local peer must then process every captured packet and return valid response packets. If the peer is not running, captured network requests will time out.
Turn off Route all traffic while developing the transport if only traffic addressed to the configured tunnel subnet should enter packetFlow.
Requirements:
- Xcode 15 or newer.
- iOS 16 or newer.
- A physical iPhone or iPad. Packet-tunnel behavior cannot be validated in the simulator.
- An Apple Developer team whose App ID and provisioning profiles include the Network Extensions / Packet Tunnel Provider capability.
First configure signing:
cp Config/Signing.xcconfig.example Config/Signing.xcconfigEdit Config/Signing.xcconfig with the team ID and a globally unique app bundle identifier. The extension identifier is automatically derived as <app-id>.PacketTunnel.
An unsigned compile, useful for finding source or project errors, does not need signing:
./scripts/build-unsigned.shFor sideloading workflows that re-sign an IPA, first embed ad-hoc signatures so the signer can preserve the Network Extension entitlement on both the containing app and extension:
xcodebuild \
-project PacketBridge.xcodeproj \
-scheme PacketBridge \
-configuration Release \
-sdk iphoneos \
-destination "generic/platform=iOS" \
-derivedDataPath build/DerivedDataRelease \
CODE_SIGNING_ALLOWED=NO \
clean build
./scripts/package-adhoc.sh Release build/DerivedDataReleaseIf the containing app loses com.apple.developer.networking.networkextension
during re-signing, the VPN can still appear connected while iOS rejects
sendProviderMessage IPC, preventing Runtime status from updating.
For a signed archive:
PACKETBRIDGE_TEAM_ID=ABCDE12345 \
PACKETBRIDGE_BUNDLE_ID=com.yourcompany.PacketBridge \
./scripts/archive.shThe signing certificate and provisioning access must already be available to the SSH account's keychain. If Xcode must create/update profiles, run:
PACKETBRIDGE_ALLOW_PROVISIONING_UPDATES=1 \
PACKETBRIDGE_TEAM_ID=ABCDE12345 \
PACKETBRIDGE_BUNDLE_ID=com.yourcompany.PacketBridge \
./scripts/archive.shThe archive is written under build/PacketBridge.xcarchive. No graphical Xcode interaction is needed.
- Install and launch the signed app on a physical device.
- Set ports and tunnel addresses.
- Start the local peer/proxy, with its raw bridge socket bound to the configured peer port.
- Tap Save & Connect and approve the iOS VPN configuration prompt.
- Point local proxy clients at
127.0.0.1:<SOCKS5 port>. - Check live packet and SOCKS connection counters in the app.
Runtime status is always visible directly below the connection controls. While connected, it refreshes once per second and shows the packet-tunnel extension's current resident memory, open SOCKS connections versus the configured limit, active UDP associations, and packet/byte counters. Before live data is available, it shows disconnected or waiting placeholders instead of disappearing.
The first VPN approval is an iOS security prompt and must be accepted on the device. SSH access to the builder cannot approve that device-side prompt.
PacketBridgeApp/: SwiftUI configuration and status UI, plusNETunnelProviderManagerlifecycle.PacketTunnelExtension/PacketTunnelProvider.swift: network settings, service lifecycle, and runtime status IPC.PacketTunnelExtension/SOCKS5Server.swift: TCP and UDP SOCKS5 sessions and internet relays.PacketTunnelExtension/SOCKS5Protocol.swift: SOCKS5 wire parsing and encoding.PacketTunnelExtension/PacketUDPBridge.swift: raw packet transport betweenpacketFlowand connected UDP.Shared/: property-list-safe configuration and status models compiled into both targets.
The server intentionally supports no authentication because it is loopback-only. Do not change the listener host to a wildcard address without adding authentication and access control. This is a development networking tool, not an encrypted VPN protocol; the raw UDP bridge provides no confidentiality by itself.