A Swift/C VPN engine for iOS and macOS that speaks Xray-compatible protocols straight through Apple's NEPacketTunnelProvider — without gomobile or an embedded Go core.
Why · Features · Install · Integrate · Transport status · Limits
Warning
Alpha. The engine has run on real iOS devices with VLESS+gRPC and VLESS+XHTTP. Reality and Shadowsocks are partial (see Transport status). The public API is not stable yet.
Apple runs a Packet Tunnel in an app extension with a hard memory ceiling — historically 15 MB, raised to at most 50 MB on recent iOS. Cross the line and the system kills the tunnel process. To the user that reads as "the VPN keeps dropping every few minutes under real traffic."
Most iOS Xray clients embed xray-core through gomobile, and that is where the budget goes:
- the Go runtime reserves a large slice of resident memory before a single byte of user traffic flows;
- Go's garbage collector spikes under bursty load and pushes the extension past the ceiling;
- connection state and DNS caches grow because the Go stack was never tuned for this sandbox.
NEAXCE takes Go out of the picture. Protocols and transports are plain Swift. The userspace TCP/IP stack that bridges the TUN device into per-flow streams is plain C (lwIP). The engine idles at a few MB and keeps headroom for dozens of concurrent connections inside the 50 MB budget — so the tunnel stays up and flows stop tearing.
|
🧠 Memory-first by design |
🔌 No Go, anywhere |
|
🕳️ No DNS leaks |
🔀 Real Xray transports |
|
🧱 Userspace TCP/IP |
🍎 Drop-in provider |
- Swift 5.9+ to build the library. Running the test suite needs Swift 6 / Xcode 16 (the tests use swift-testing).
- Apple platforms: iOS 15+, macOS 12+, tvOS 15+.
- Entitlement: the host app needs
com.apple.developer.networking.networkextensionwith the valuepacket-tunnel-provider. Apple grants it on request; it is not on by default.
Add the package to your Packet Tunnel Extension target with Swift Package Manager:
// Package.swift
.package(url: "https://github.com/pinusmassoniana/NEAXCE.git", branch: "main").target(
name: "MyPacketTunnel",
dependencies: [
.product(name: "NEAXCE", package: "NEAXCE")
]
)The Examples/BasicIntegration folder has copy-paste reference code for all three.
Subclass NEAXCETunnelProvider and hand it your App Group. This is the only code the extension needs:
import NEAXCE
final class PacketTunnelProvider: NEAXCETunnelProvider {
override var configuration: NEAXCEConfiguration {
NEAXCEConfiguration(
appGroupIdentifier: "group.com.yourcompany.YourVPNApp",
loggerSubsystem: "com.yourcompany.vpn"
)
}
}Set NSExtensionPrincipalClass in the extension's Info.plist to $(PRODUCT_MODULE_NAME).PacketTunnelProvider.
TunnelManager.swift is a NETunnelProviderManager wrapper that creates or loads the VPN profile, encodes a VPNConfig into providerConfiguration, and starts, stops, and observes status.
VPNConfig is the wire format. Build it by hand, or parse a subscription URI with the bundled URIParser.swift (handles vless:// and ss://).
let config = VPNConfig(
serverHost: "vpn.example.com",
serverPort: 443,
serviceName: "vless-grpc",
uuid: "4c4c6842-0000-0000-0000-000000000000",
transportType: .grpc,
path: nil
)
try await tunnelManager.connect(config: config)| Transport | Status | Notes |
|---|---|---|
| VLESS + gRPC + TLS | ✅ working | Primary exercise path |
| VLESS + XHTTP + TLS | ✅ working | HTTP/2 POST+GET split |
| Shadowsocks (AEAD) | 🚧 partial | chacha20-ietf-poly1305, aes-256-gcm |
| Reality (XTLS) | 🚧 partial | Handshake + record layer; no XTLS flow |
Tip
On .reality and .shadowsocks, DNS is tunnelled through the encrypted transport by default. Plaintext DNS forwarding is opt-in only, behind allowInsecureDNS — so DNS fails closed rather than leaking if no encrypted path exists.
What's in the box — layer-by-layer
| Layer | Component |
|---|---|
| TCP/IP stack | lwIP (BSD-3, vendored) with a NAT-rewrite trick, so TUN TCP flows terminate in userspace |
| Protocols | VLESS (full); Shadowsocks AEAD (chacha20-ietf-poly1305, aes-256-gcm) |
| Transports | gRPC over HTTP/2+TLS; XHTTP over HTTP/2+TLS; Reality (TLS 1.3 ClientHello forgery, partial) |
| DNS | DNS-over-TCP through the active transport (no leaks); optional opt-in forwarder |
| Provider | NEAXCETunnelProvider : NEPacketTunnelProvider with path reassert, sleep/wake, optional App Group stats |
Project layout
Sources/
├── CLwIP/ Vendored lwIP + thin C helpers
│ ├── include/ Public headers (module.modulemap)
│ ├── core/ lwIP core (ipv4/, ipv6/)
│ ├── LwIPHelpers.c Swift-facing C shims + NAT rewrite
│ └── sys_arch.c NO_SYS=1 platform glue
└── NEAXCE/
├── Core/ Provider, engine, config, per-flow state
├── LwIP/ Swift wrapper over CLwIP
├── DNS/ DNS proxies (VLESS / Shadowsocks / Reality / opt-in direct)
├── Crypto/ Reality handshake, TLS 1.3 primitives
├── Protocols/ VLESS + Shadowsocks codecs
├── Transports/ gRPC, XHTTP, Reality
└── Logging/ Optional rolling file logger
Tests/NEAXCETests/ Unit tests (XCTest + swift-testing)
Examples/BasicIntegration/ Reference SwiftUI plumbing
- Reality is partial: the handshake and TLS 1.3 record layer work, but the XTLS vision flow and connection migration are still open.
- No HTTP/2 multiplexing. Each VLESS stream opens its own TLS connection; connection pooling is planned.
- No UDP over gRPC/XHTTP. UDP from the TUN is dropped; the NAT-rewrite is TCP-only by design.
- One skipped test (
VLESSCodecTests.testVisionProcessorsHandleFragmentedInput) marks the vision-processor fragmentation gap. It isXCTSkip-ped with a reason so CI stays green, and stands as a visible TODO for the vision refactor.
See CONTRIBUTING.md. Report security issues through SECURITY.md, not public issues — a bug here can leak traffic or expose a user's address.
Apache License 2.0. See LICENSE. Third-party terms, including lwIP's BSD-3-Clause, are in THIRD_PARTY_LICENSES.md.