flutter_sangfor 0.0.14
flutter_sangfor: ^0.0.14 copied to clipboard
Flutter integration for Sangfor aTrust and Easy Connect VPNs with an independently implemented protocol core
0.0.14 #
SangforTunnelManager.installre-applies the currentlocalizedDescriptionandserverAddressto a configuration an older build already saved, and re-saves only when they differ. iOS reuses a saved VPN configuration instead of rebuilding it, so without this a device that added the VPN before 0.0.13 kept the old entry name -- including theflutter_sangforserver field that 0.0.13 stopped defaulting to. Updating a saved configuration raises no permission prompt, so an upgrade renames it in place on the next install.
0.0.13 #
- iOS packet tunnel extension: an in-extension HTTP proxy, published to the
system with
NEProxySettings, so apps that honor the system proxy reach the gateway's resources by host name -- wildcard and domain-published ones the routing table cannot express -- while the tunnel keeps running after iOS suspends the app. It is opt-in: the session plan's newdomainRoutingkey carries the route policy (followServer/custom), the custom entries and the hosts that must stay direct; a plan without the key gets no proxy. SangforRouteMatcherports the app'sVpnRouteMatcher(the Android loopback proxy's rule) to Swift, driven by the sameroute_matcher_cases.jsonas the Dart tests.SangforProxyPolicyandSangforHttpProxyParserare pure and covered bytool/run_swift_tests.sh.- The proxy parser decodes the request head byte-for-byte instead of with
String(data:encoding:.isoLatin1): Linux's swift-corelibs-foundation answers that with nil whenever a header carries a control byte (CR/LF), so every request with a header line was refused there while the same code passed on Apple platforms. The network checks needNetwork.framework, sorun_swift_tests.shnow runs them only on an Apple host and skips them elsewhere instead of failing the portable core checks on Linux CI. - The proxy dials the TCP tunnel without claiming a resolved address, as the Dart in-app proxy does.
- A start the system initiates (Settings, Control Center, iOS restarting the
provider after a network change) used to fall back to the loopback bridge
with a placeholder address and no routes: a tunnel that reported connected and
carried nothing. The extension now rebuilds its settings from the session
plan's new
tunnelSettingskey, or fails with error 200 and a reason. - The plan's
tunnelSettings.excludedRoutesare applied asNEIPv4Settings.excludedRoutes, together with the gateway nodes' addresses, so the destinations the app lists (the VPN gateway) and the tunnel's own transport can never enter the tunnel. - When the gateway assigns an interface address other than the one the tunnel was configured with, the extension re-applies its settings with the assigned one instead of leaving replies addressed to a stranger.
- A TCP tunnel dial to a host known by name no longer carries a
destIP. The terminator sent the host name asdestIPwhen the resource was published withoutaddrPretend, and the gateway closed the connection during the handshake; that is what brokedatarepo.whu.edu.cn-style hosts (100 of the 772 routes on the WHU gateway). Probed against the live gateway: a request naming the host with nodestIPis accepted for resources with and withoutaddrPretend, while anydestIPis refused (tcp tunnel connection not allowedfor the address the name resolves to). A flow to a bare address, with no name behind it, still carries that address when the resource is notaddrPretend. - Flows to an address the plan pre-resolved from a published host name are now
decided by the host name they turn out to want, not by the address alone.
Several names often sit behind one address (a shared front end) and only some
are published --
user-serv.whu.edu.cnshares an address withehall.whu.edu.cn-- so the address could only say "ehall" and the flow was dialed through the tunnel asehall, where nothing serves the other name. The terminator now holds the dial until the client's first bytes arrive and reads the host from them (SangforFlowSniffer: the SNI of a TLS ClientHello, theHostof an HTTP request). A name a resource covers on that port goes through the tunnel, dialed by that name; any other is carried out directly (SangforDirectStream,Configuration.directDialer), as it would have gone without the tunnel. When nothing names a host (the server speaks first, ECH, no SNI) a short wait ends and the address's own name decides, as before. Nothing unpublished is carried by the tunnel, and an address no name resolved to is still dropped. The plan's optionaldialHostAliaseslists every name behind an address so a flow is matched to the name that covers its port. - Switching the VPN off in Settings and on again works. A stop the user made
wiped the session plan, so the start that followed (which arrives with no
options and can only run from the plan) found nothing and failed with "no
tunnel settings in the session plan". A plan may now carry
keepOnUserStop: the extension then keeps it across a user stop. The app still clears it itself when it disconnects, andproviderDisabled/appUpdatestill wipe it. - A session the gateway has dropped is now reported as one. A refused L3
handshake (
statusorcodenot zero) was an ordinary protocol error, so an extension whose session had expired reconnected every five seconds forever while the system showed it as connected. It is nowtunnelAuthFailed: a start that hits it fails with "The VPN session is no longer valid" (error 201); on a reconnect the first refusal is retried (a node can be busy) and the second in a row stops the tunnel, wipes the plan and cancels with the same error. An error on a connection that never finished its handshake is now reported once, through its start completion, instead of also through the connection's error callback. - Connections carried outside the tunnel refuse the tunnel's own interface (a
utun is an
.otherinterface), so they cannot be routed back into it. Each sniffed flow's decision and the first direct connections (with the interface they used) are logged, anddirectjoins the data plane statistics. - The proxy's direct connections now go through
SangforDirectStreamlike the terminator's, instead of a second hand-writtenNWConnectionrelay, so there is one direct path to reason about. A direct dial that is refused or has no route fails at once (the framework would otherwise sit inwaitinguntil the timeout).swift-tests/networkexercises both against real loopback sockets, including the proxy's direct, tunnel, refused and malformed-request paths. - Failed tunnel dials, and the first forty successful ones, log the destination,
the
destIPsent, the resource'saddrPretendand its app. - The extension logs how node certificates are checked (pinned, or not verified because the gateway advertised no pins).
SangforTunnelManager.loadreports the tunnel's status without creating a configuration (.invalidwhen none is saved), so an app can find at launch a tunnel that outlived it, or that the user switched on in Settings, without raising the VPN permission prompt.- The extension's
getStatsreply carries asessionsummary (interface address, DNS servers, user name) for an app taking over a tunnel it did not start in this process. - The terminator resets a segment of a TCP flow it does not hold. After the VPN
is switched off and on again the client's old connections retransmitted into
nothing until their own timeout (
packet not routed); they now reconnect at once. keepOnUserStopcovers a stop the app makes too: the app that sets it owns the plan's lifetime. A removed configuration wipes the plan like a disabled provider does, and stop reasons 9-13 are logged by name.- The iOS VPN configuration no longer names itself after the package.
SangforTunnelManagerdefaulted bothlocalizedDescriptionand theNETunnelProviderProtocol.serverAddressthat iOS requires to"flutter_sangfor", and iOS shows those strings verbatim in Settings > VPN, so every consumer leaked the dependency name (the entry title and its "Server" field). The Dart argument defaulted to'flutter_sangfor'too. IosVpnDevice.start,startNative,installConfigurationandstartArgumentsnow take an optionalserverAddress, andlocalizedDescriptionis optional. OmittinglocalizedDescriptionmakes the iOS side use the app's localizedCFBundleDisplayName, so the VPN entry follows the device language; omittingserverAddressfalls back to that entry name. iOS freezes the value when the configuration is first saved, so a later language change needs a re-save to show through.- Android's
VpnTunnelServicehardcodedVpnService.Builder.setSessionto"flutter_sangfor", which the system VPN UI shows.AndroidVpnDevice.startnow takes an optionalsessionName; when omitted the service uses the app's localized label (PackageManager.getApplicationLabel), so the session name follows the device language and never leaks the package name.
0.0.12 #
VpnTunnelServiceoverridesonRevoke()and reports it to Dart asAndroidVpnDevice.revocations. Until now the system taking the tunnel away -- the user switching the VPN off in system settings or from the quick-settings tile, or anotherVpnServicetaking over -- was invisible to the app: no event fired, the packet loop kept reading a descriptor the kernel had already closed, andgetState()kept answeringconnected.- The revocation is reported from
onRevoke()rather thanonDestroy(), becauseonDestroyalso runs on an ordinary in-appvpnStopand would announce a revocation on every normal disconnect. For the same reason it clears the cached state behindgetState(): unlike a disconnect the app asked for, nothing follows a revocation with avpnStop. - Both
flutter_sangfor/servicestreams install the single native handler they share, sorevocationsworks on its own instead of depending on something having subscribed todisconnectRequestsfirst.
0.0.11 #
- Fix iOS builds by distinguishing manager and data plane errors and converting
the TLS trust wrapper to
SecTrustfor certificate pinning. Keep IPv6 parsing compatible with the package's iOS 15 deployment target.
0.0.10 #
SangforTunnelInstalledDaemon.diagnoseUnavailable()explains why there is no daemon, instead of the barenullthatconnect()returns so callers can fall back quietly. The causes need different fixes and look identical from the outside: nothing installed, a configuration written by another version, an app update that moved the executable out from under the logon task, and a task that has not run since it was installed. Returnsnullwhen a daemon is reachable, so it is safe to call unconditionally.SangforTunnelHostConfig.installedFromcarries the path the daemon's--installrecorded. The daemon warns at startup when it is running from somewhere else, which is what makes a relocated install visible rather than a tunnel that silently stops working at the next logon.
0.0.9 #
- Add a client for
sangfor-tunneld, the process that runs the Rust data plane outside the app:SangforTunnelDaemon, with two ways to get one.SangforTunnelInstalledDaemon.connect()finds a daemon installed as an elevated logon task by reading the configuration in the user's profile, which is what removes the elevation requirement on Windows — creating a wintun adapter needs an elevated process, and an app that runsasInvokercannot do it at all.SangforTunnelDaemonProcess.start()launches the binary as a child, which inherits the app's privileges and so is forloopbackand--dry-runwork. - Both drive the same control protocol —
start,status,stopSession,stop— over a token-gated loopback socket. The daemon outlives its sessions, so one elevated process serves repeated connect/disconnect cycles. SangforTunnelHostConfigandSangforTunnelSnapshotmodel the daemon's two documents. Decoding is lenient where the daemon's own parsing is strict, so a newer daemon that adds a field does not break an older app.- A session plan is handed over by path, never inline: it carries the request signing key, and the control channel is reachable by any local process that has the token.
0.0.8 #
- Keep the extension-native tunnel alive across a restart: the session plan is written with "complete until first user authentication" protection (iOS may bring the tunnel back after a reboot or a network change with the device still locked) and is only wiped on a user-initiated, provider-disabled, or app-update stop instead of every stop.
0.0.7 #
- Add the iOS extension-native data plane:
IosVpnRuntimeMode.extensionNative,IosVpnDevice.startNative, andwriteSessionPlan/clearSessionPlanhand a session plan to the packet tunnel extension through the App Group, and the extension then runs the tunnel itself — L3 handshake, per-flow auth, heartbeats, reconnect, TCP-tunnel dials, and local TCP termination. The VPN keeps carrying traffic when iOS suspends the app, which the loopback bridge could never do. The bridge stays the default. - The native core is pure Swift and verified off-device: SHA-256/HMAC, a JSON
writer that reproduces Dart's
jsonEncodebyte for byte, the L3 and TCP tunnel frame codecs, the IPv4/TCP builder with full checksums, route matching, the flow tracker, the userspace TCP terminator, and the L3 connection driver.tool/run_swift_tests.sh(or.ps1) checks all of it against golden fixtures emitted by the Dart reference implementation. - Fix a TCP terminator stall: the ACK comparison was inverted, so a cumulative ACK covering more than one segment never advanced the send window and the flow hung after its first window of data.
- Pin node certificates with the gateway's salted digest
(
SangforCertificateDigest) instead of a plain SHA-256 of the DER. - Advertise the system proxy only in loopback-bridge mode; the native plane terminates those flows itself and must not point apps at a proxy that only exists while the Runner is awake.
0.0.7 #
- Add the iOS extension-native data plane: the packet tunnel extension can run
the tunnel itself (
IosVpnRuntimeMode.extensionNative,IosVpnDevice.startNative) from a session plan handed over through the App Group (IosVpnDevice.writeSessionPlan/clearSessionPlan), so the VPN survives the Runner being suspended. The loopback bridge stays the default. - Add the pure-Swift core that plane runs on: SHA-256/HMAC, a canonical JSON
writer that matches Dart's
jsonEncodebyte for byte, the L3 and TCP-tunnel frame codecs, the IPv4/TCP packet builder with full checksums, route matching, the userspace TCP terminator, and the L3 connection driver with per-flow auth, heartbeats, and reconnect.tool/run_swift_tests.shcompiles and checks all of it against golden fixtures emitted by the Dart reference implementation, on Linux, Windows, and macOS. - Fix a TCP terminator stall: the acknowledgement comparison was inverted, so a cumulative ACK covering more than one segment never advanced the send window and the connection hung after its first window of data.
- Pin node certificates with the gateway's salted digest
(
SangforCertificateDigest) rather than a plain SHA-256 of the DER.
0.0.6 #
- Add the userspace TCP terminator (
SangforTcpTerminator,SangforTerminatingTunnel): a packet device can now serve TCP flows the tunnel refuses to forward as raw IP by completing the handshake locally and relaying bytes through aSangforTcpDialer. Ships an IPv4/TCP codec with full checksums, MSS clamping, retransmission with exponential backoff, peer window flow control, and upstream backpressure. - Add
SangforSystemProxy, which publishes a loopback HTTP proxy as the OS-wide proxy on Windows (WinINET keys underHKCU, no elevation needed) and macOS (networksetup, administrator rights), capturing and restoring whatever the user had configured before. - Advertise a system proxy from the iOS packet tunnel:
IosVpnDevice.start(proxyHost:, proxyPort:)now reachesNEProxySettingswith an all-hosts match domain, the counterpart of Android'sVpnService.Builder.setHttpProxy.
0.0.5 #
- Fix the ohos ArkTS build: NetAddress.family is a plain number in the OpenHarmony SDK (no connection.NetFamily enum), RouteInfo.gateway is mandatory, and Index.ets imports must precede exports.
0.0.4 #
- Add the HarmonyOS VpnExtensionAbility adapter (ohos module, SCM_RIGHTS TUN fd hand-off) and the OhosVpnDevice Dart adapter.
0.0.3 #
- Fix an Android ANR: establish the tunnel from a background executor instead of blocking the platform thread while the service comes up.
- Fall back to the underlying network's DNS resolvers when the server publishes none, and keep those resolvers outside the TUN routes.
- Advertise the caller's loopback HTTP proxy as the VPN system proxy on Android 13+ so domain-published resources work in system mode.
- Add a persistent notification with live speed, uptime, and a disconnect action; dismissed notifications recover via the delete intent.
- Add
AndroidVpnDevice.updateStatsanddisconnectRequestsplumbing. - BREAKING CHANGE: rename the Android namespace from
com.tsinbeilabs.flutter_sangfortocom.tsinbei.flutter_sangfor.
0.0.2 #
- Add library-level dartdoc for the public API surface.
- Update the package description to cover aTrust and Easy Connect.
0.0.1 #
- Wire
getStateanddisconnectacross all declared platforms. - Validate connection arguments in the Dart method-channel adapter.
- Return an explicit unsupported error until the protocol transport is ready.
- Add the platform-neutral
SangforAuthRequesttransport boundary. - Add
flutter_sangfor_atrustandflutter_sangfor_easy_connectpackage boundaries. - Add aTrust manifest and authentication-method discovery over HTTPS.
- Add aTrust primary password exchange with server-provided RSA parameters.
- Add aTrust MFA-step normalization and L3VPN resource parsing.
- Add aTrust SMS verification and client-resource retrieval requests.
- Add aTrust Cookie/SID persistence, environment reporting, and online info.
- Add an end-to-end aTrust password and SMS login coordinator.
- Add explicit TOTP, RADIUS, and generic code challenge callbacks.
- Add access checks, enhanced-auth continuation, and current-device binding.
- Add credential-free session snapshots and WAN/LAN node-group parsing.
- Add a bounded incremental tunnel-frame boundary and node ordering.
- Add the aTrust L3 tunnel with keep-alives and dual packet-stream demux.
- Add the aTrust TCP tunnel channels with
dialTcpfor user-space sockets. - Add the EasyConnect login, conf parsing, TLS 1.1/1.2, token, RX/TX streams, and heartbeat keep-alive.
- Add the userspace data plane:
SangforTcpStream,SangforPacketTunnel,SangforPacketDevice, and theSangforTunnelRouterbidirectional pump with egress/ingress filters and cancellation-token support. - Add
SangforSocks5Server(RFC 1928 no-auth CONNECT) with pipelined-request buffering, serialized sends, and cancellation-token support. - Add EasyConnect TCP-over-L3 synthesis: IPv4/TCP/UDP packet building,
DNS query/parse over the tunnel, a retransmitting client TCP state machine
with zero-window probing, and a
dialTcpconnector API. - Add platform TUN adapters:
WintunDevice(Windows),TunDevice(Linux),AndroidVpnDevice(VpnService),UtunDevice(macOS), andIosVpnDevice(NetworkExtension packet tunnel with loopback IPC).