Implement initial proof-of-concept for split tunnel functionality on Windows,
allowing applications to route traffic through a designated network interface
while bypassing default system routing.
Features:
- Split tunnel module with TCP/UDP proxy infrastructure
- Firewall integration with split tunnel verdict handling
- SplitTunneling context attached to connections
- Configuration options: enable toggle, interface selection, and policy rules
- UI display of split tunnel connection details in connection info panel
- Subsystem configuration for user-level access
Windows-specific implementation:
- Uses proxy-based interface routing on Windows
- Automatic or manual interface detection and binding
- Support for IPv4 and IPv6 traffic
Note: Linux implementation is under development. SPN takes precedence over
split tunnel when both are enabled, ensuring SPN connections bypass this feature.
- Refactor GetInterface* functions to return InterfaceInfo with IPv4/IPv6
addresses instead of just net.Interface
- Add pre-caching of first routable IPv4/IPv6 per interface to avoid repeated
address list scans
- Skip loopback interfaces in cache refresh
- Add GetBestPhysicalDefaultInterfaces() to detect which physical adapters
carry the default route per IP family, excluding VPNs/tunnels
- Implement platform-specific physical interface detection:
* Linux: reads /proc/net/route and /proc/net/ipv6_route, uses
/sys/class/net/*/device to identify real hardware
* Windows: uses GetAdaptersAddresses with IfType filtering
* Other platforms: returns not-supported error
- Add helper functions: buildInterfaceInfo, interfaceToInfo, buildInterfaceInfoDirect,
hasRoutableIPv4, hasRoutableIPv6
- Update tests to work with new InterfaceInfo return type and add coverage
for new features
Add interfaces.go with GetInterface, GetInterfaceByIP, GetInterfaceByMAC
and GetInterfaceByName for resolving local network interfaces by IP, MAC,
or name.
- Lazy init: no work until first call
- sync.RWMutex with double-checked locking for concurrent read throughput
- Refresh throttled to once per second to absorb rapid interface churn
(same NetworkChangedFlag pattern used across netenv)
- Only live, routable interfaces cached: FlagUp required; link-local and
address-less interfaces excluded as unsuitable for TCP/UDP tunneling
Add a new verdict (value 8) for routing connections through the split
tunnel. This prepares the infrastructure for the upcoming split-tunneling
feature without implementing the full feature yet.
Changes:
- Define VerdictRerouteToSplitTun in network/status.go with String() and Verb()
- Add RerouteToSplitTun() to the Packet interface and InfoPacket stub
- Implement RerouteToSplitTun() for windowskext (v1) and windowskext2 (v2) packets
- Map VerdictRerouteToSplitTun to KextVerdict 11 in kextinterface and kext2
- Handle the verdict in packet_handler.go dispatch, connection.go, api.go,
metrics.go and nameserver.go
- Add VerdictRerouteToSplitTun = 8 to Angular Verdict enum and update
stats counting, filter queries and verdict CSS class
(WIP) Note: Linux (nfq) implementation not updated yet. Therefore Linux build will fail.
- Change DeciderFunc signature to return (remoteIP net.IP,
remotePort uint16, localAddr string, extraInfo any, err error)
instead of a single "host:port" dest string
- Extract ConnContext, Metrics, sessionCache, and idCounter into
a new cache.go file
- Add a secondary destKey index to sessionCache for O(1)
FindProxiedEgressConnection lookups by upstream destination
- Attach per-session extraInfo and atomic byte/packet counters
to ConnContext
- Update TCP and UDP proxies, tests, and README accordingly
The changes add support for tracking and properly routing connections to the IVPN client's local service port,
which is needed when the default firewall action is to block unknown connections.
The verdict handler is now registered earlier in the connection flow to handle connections while the client is connecting.
https://github.com/safing/portmaster-shadow/issues/34
Access option 'spn/enable' only after full initialisation by replacing
the eagerly-initialised struct field (config.GetAsBool called at
construction time) with local variables declared at the call-site of
each function that needs the option.
Affected: service/control, service/interop/ivpn
When IVPN connects, virtual interface IPs can cause netenv to detect
the VPN egress as the device's physical location, leading to a wrong
SPN home hub selection. Fix this by gating interface- and traceroute-
based location methods behind a new flag.
- Add DisableNetworkDerivedLocation(bool) to netenv/location.go backed
by an atomic.Bool; getLocationFromInterfaces and getLocationFromTraceroute
return early when the flag is set
- Call DisableNetworkDerivedLocation(true) in onConnectionStarting and
DisableNetworkDerivedLocation(false) in onConnectionStopped and in the
connectIvpnClient defer (covers unexpected client disconnects)
https://github.com/safing/portmaster-shadow/issues/34
Add a Windows-specific hook that calls conf.SetBindAddr with the default
physical interface's addresses when SPN is connecting. This ensures SPN
hub traffic bypasses the IVPN tunnel rather than being routed through it.
- Add hook_windows.go with spnConnectingHook that reads the default
interface via netenv.GetDefaultInterface and sets the SPN bind address
- Update hook_default.go build constraint to exclude windows
- Export DefaultNetInterface and GetDefaultInterface in netenv to allow
cross-package access from the ivpn interop layer
https://github.com/safing/portmaster-shadow/issues/34
Introduce mark 1709 (MarkAcceptFinal) and a corresponding
PermanentAcceptFinal() method that sets this mark on packets belonging
to Portmaster-owned outbound connections.
Add iptables rules (both IPv4 and IPv6, filter and mangle chains) to
ACCEPT packets/connections carrying mark 1709, so further OUTPUT rules from
third-party software (e.g. iVPN) cannot override the allow decision.
https://github.com/safing/portmaster-shadow/issues/34
Add a synchronous HookMgr[T] that lets callers register pre-connect
hooks before SPN dials a home hub. The IVPN interop layer subscribes
to this hook and uses Linux ip-rule/ip-route to steer SPN hub IPs
through a dedicated routing table (717) pointing to the non-VPN default
gateway, preventing SPN control traffic from being tunnelled into IVPN.
- service/mgr: add generic HookMgr[T] (synchronous, cancellable)
- spn/captain: expose HookSPNConnecting; invoke it in connectToHomeHub
- service/netenv: add GatewayInfo + GatewaysInfo() with interface/mask
- service/interop/ivpn: add ensureSpnHubBypassVpnRoutes managing policy
routing; call it from the SPN pre-connect hook and on VPN stop/connect
- nfq/packet: add hex comments next to mark constants
https://github.com/safing/portmaster-shadow/issues/34
When nft/nftables is unavailable on a Linux host, wg-quick falls back to
installing equivalent kill-switch rules via iptables (raw/PREROUTING).
This change adds support for that fallback path so SPN reverse-NAT loopback
traffic is allowed in both backends, while keeping rule scope narrow and
cleanup idempotent.
- extend SPN compatibility reconcile logic to support both nft and iptables backends
- keep nft path as preferred behavior and continue tracking/removing inserted nft rule by handle
- add strict iptables raw PREROUTING fallback rule for loopback reverse-NAT traffic when nft is unavailable
- make rule lifecycle idempotent by cleaning previously managed rules before re-adding on state changes
- refresh inline documentation to describe dual-backend behavior and safety scope
- ensure compatibility rules are reconciled/removed on IVPN disconnect and failed connection teardown
https://github.com/safing/portmaster-shadow/issues/34
- split IVPN event handlers into dedicated file and add ConnectedResp handling
- store connected session details in client status for follow-up compatibility logic
- add Linux-only SPN compatibility hook that reconciles an nft allow rule for WG loopback reverse-NAT traffic
- track and clean up inserted nft rule handles to avoid stale rules
- add non-Linux no-op hook implementation and trigger compatibility reconciliation on config changes
https://github.com/safing/portmaster-shadow/issues/34
Previously, the external verdict handler was placed in filterHandler,
which is only called for new packets. This meant it was silently
bypassed when connections were re-evaluated via resetConnectionVerdict.
Move the handler into FilterConnection so it is consistently applied
for all filtering paths, including verdict resets.
https://github.com/safing/portmaster-shadow/issues/34
Add ensureJumpRulesAtTop and reinsertDisplacedRules to detect and fix
iptables jump rules that have been displaced by other services during
boot. A background worker runs checks at 5s, 15s, and 45s after nfqueue
interception starts, reinserting any out-of-position rules.
- fixed issue where downgrades were blocked due to an overly broad
Published date check
- added symmetric version/date mismatch checks for both upgrades and
downgrades, skipped when current index is locally generated
- locally generated indexes now follow the standard upgrade path;
upgrading to the same version is explicitly blocked
https://github.com/safing/portmaster-shadow/issues/39
Add "Don't show again" action to the stale cache notification.
Suppression state is stored in the database and checked on startup.
System notification is shown only on first occurrence.
Reset handler in broadcasts now also clears the suppression record.
https://github.com/safing/portmaster/issues/2061
When Portmaster connects to the IVPN Client, display an info notification
informing the user that IVPN connections are allowed and that DNS will be
handled by Portmaster's local resolver when configured.
The notification includes a "Do not notify me anymore" action that
permanently suppresses future notifications by writing a marker record to
the core database. The check runs before showing the notification on each
subsequent connection.
The "Reset Notification States" API endpoint (and matching UI menu item)
now also clears the IVPN suppression record alongside the broadcast states,
so all suppressed notifications can be restored at once.
- service/interop/ivpn: add notification.go with initAndShowNotification,
isNotificationSuppressed, and suppressNotification
- service/interop/ivpn/ivpn.go: show notification on connect if not suppressed
- service/broadcasts/api.go: extend reset-state handler to also delete the
IVPN suppression record; update endpoint name and description
- desktop: rename "Reset Broadcast State" menu item and toast messages to
"Reset Notifications State"
Introduces a standalone Go module with a minimal Layer-4 proxy used by
the split-tunnelling subsystem:
- DeciderFunc injects routing and optional source-address binding per
session, enabling per-connection traffic steering.
- TCPProxy: accept loop, bidirectional pipe with pooled 32 KiB buffers,
rolling read/write deadlines, half-close propagation, and graceful
shutdown via context cancellation.
- UDPProxy: single listen socket with a NAT-like session table keyed by
client address, double-checked locking for burst safety, idle eviction
loop, and per-session upstream sockets.
- Shared Config (MaxSessions, ReadTimeout, WriteTimeout, BufferSize,
DialTimeout), ConnContext with atomic byte/packet counters, and a
sessionCache with aggregate Metrics.
- Full test suite (functional + race) and benchmarks for throughput and
session creation cost.
Bugs fixed:
- Inbound UDP from VPN server incorrectly blocked
- Firewall verdicts possible before client status initialized
Also: move interop before interception in startup order, simplify DNS state tracking.
Add EventStartStopState event manager and IsStarted() method
to the Interception module so other modules can react to start/stop state changes.
Update IVPN interop to subscribe to interception start/stop events
and skip applying custom DNS settings when interception is inactive,
ensuring correct behavior when Portmaster is in the Paused state.
Every connection verdict previously acquired an RWMutex to read IVPN
state. Replace it with atomic.Pointer[clientStatus] using an immutable
snapshot (copy-on-write) so reads are a single pointer load with no
locking on the per-packet hot path.
Apply the same pattern to the external verdict handler: replace a
data-racy plain function variable and two auxiliary atomic.Bool flags
with a single atomic.Pointer[ExtVerdictHandlerFunc]. Use CompareAndSwap
for set-once semantics. Move the load into the default branch of
filterHandler so pre-authenticated and DNS-redirect connections pay zero
cost.
Allow Portmaster to cooperate with the IVPN client:
- Accept IVPN VPN tunnel and service process connections
- Delegate DNS control to Portmaster when custom DNS is configured
- Auto-connect to IVPN daemon on startup and on ping
- Hook into firewall verdict pipeline via new ExtVerdictHandler