Skip to content

Feature/split tunneling - #2182

Merged
stenya merged 33 commits into
developmentfrom
feature/split-tunneling
Jun 11, 2026
Merged

Feature/split tunneling#2182
stenya merged 33 commits into
developmentfrom
feature/split-tunneling

Conversation

@stenya

@stenya stenya commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

https://github.com/safing/portmaster-shadow/issues/45

Summary by CodeRabbit

  • New Features

    • Added Split Tunnel module with UI controls for enabling, configuring network interfaces, and managing routing policies.
  • Improvements

    • Log level changes now emit notification messages for visibility.
    • Enhanced connection details to display split-tunnel routing information.
    • Improved feature card navigation and dashboard header layout.

stenya and others added 30 commits March 6, 2026 17:20
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.
…cache

- 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
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.
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
- 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
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.
…l-manager panic

- proxies: shut down partially-started proxies on startup failure via
  deferred cleanup; avoid nil-manager panic in stopProxies by falling
  back to context.Background(); start UDP4 unconditionally and gate
  TCP6/UDP6 on IPv6Enabled()

- requests: add 30s TTL to pending requests to prevent memory leaks
  when OS drops a redirected connection before it reaches the proxy;
  schedule deferred cleanup via module.mgr.Go so the goroutine only
  runs when entries are registered and exits cleanly on module stop;
  add expiry check in consumeRequest as a safety net; clear map on Stop

- requests: guard against nil LocalIP on public AwaitRequest API
Adds a standalone bash script to build the Angular UI project and package it into a distributable zip. Supports --development and --interactive flags.
Introduces mark 1719 for split-tunnel rerouting, mirroring the existing SPN mark (1717).
Adds FILTER RETURN and NAT DNAT rules for both IPv4 and IPv6 targeting port 719.
Replaces scattered link-local exclusion checks with the new
isRoutableUnicastIP predicate (site-local or global scope only),
consistently applied in refreshIfaceCache, buildInterfaceInfoDirect,
hasRoutableIPv4, and hasRoutableIPv6. Updates tests accordingly.
Introduces LocalBinding{IP, Interface} to carry both source-address
and device binding in a single DeciderFunc return value. On Linux,
SO_BINDTODEVICE is applied via net.Dialer.Control before connect(2),
forcing traffic through the specified interface regardless of the
routing table. Non-Linux platforms get a no-op stub.

Wires LocalBinding through TCPProxy, UDPProxy, and splittun's
proxyDecider/AwaitRequest so split-tunnelled connections are bound
to the correct physical interface.
…amic

- SetLogLevel now writes a log line via writeLogLevelChange() so level
  transitions are always visible regardless of old/new level
- slogLevel is now a shared *slog.LevelVar; all derived loggers pick up
  changes instantly without recreating the handler
- slog.SetDefault is called only once (sync.Once) so handlers are stable
…rameter

- Logger interface changes from Debugf/Infof/Warnf/Errorf to
  Debug/Info/Warn/Error with key-value args (slog-compatible)
- NewTCPProxy, NewTCPProxyWithConfig, NewUDPProxy, NewUDPProxyWithConfig
  all gain a logPrefix string parameter
- noopLogger updated; resolveLogPrefix helper added
- README, tests, and benchmarks updated accordingly
- proxies.go: remove proxyLogger wrapper now that mgr.Manager satisfies
  the new structured Logger interface directly
- New config.go registers the "splittun/enable" boolean option
- subsystems.ts: change ToggleOptionKey from splittun/use to splittun/enable
- Module Start/Stop replaced with enable()/disable() helpers driven by
  the config option; a callback on EventConfigChange toggles state at runtime
…d Split Tunnel

- Rename ensureWgSpnCompatRule to ensureWgCompatRule to reflect that it now
  handles both SPN and Split Tunnel compatibility with WireGuard
- Add split tunnel configuration check alongside SPN check
- Update comments to clarify the rule applies to both SPN and Split Tunnel
- Ensure compatibility rule remains active when either SPN or split tunneling
  is enabled
Rename ensureSPNCompatibility to reconcileCompatibilityState and extract the
implementation logic to improve code clarity and maintainability across all
platform implementations.
…ements

Use the selected conntrack family for delete operations
so IPv6 entries are removed correctly too.
…ctivation

Add DeleteUnmarkedConnections() to purge conntrack entries with mark=0
when firewall is activated. This forces applications with existing
connections to reconnect, allowing DNAT rules (like SPN) to apply.

Without this, connections established while Portmaster was paused or
stopped would bypass DNAT because netfilter's nat table is only
traversed for new connections.

Loopback connections are excluded from deletion to avoid disconnecting
local services.

safing/portmaster-shadow#42
Add isOwnSplitTunnelProxyConnection to detect outbound connections
from Portmaster's own split-tunnel proxies. Replace the slice-returning
FindProxiedEgressConnection with a boolean HasProxiedEgressConnection
to avoid unnecessary allocations on each lookup.
Add PM_SPLIT_TUN_PORT (719) to fast_track_pm_packets so that redirected
packets arriving at the local split-tunnel proxy are permitted immediately
by the kext, matching the existing behaviour for the SPN port (717) and
the DNS port (53). This prevents internal proxy connections from being
reported to Portmaster and appearing in the connection monitor UI.

Also simplify fast_track_pm_packets by removing the redundant
match-on-direction branches, which were identical for Outbound and
Inbound.

Bump kext interface patch version to 2.1.1.0.
…he SPN"

The SPN Tunnel information is visible only when the connection has been routed through it.
- Replace "Safing Support" feature with Split Tunneling in features.go,
  using a dedicated config key/scope and free package tier
- Fix feature-card component to prioritize ConfigKey over ConfigScope
  when resolving the config lookup key
Adds a "Split Tunnel" toggle to the app profile quick-settings bar,
mirroring splittun/use per-app setting.

Shows an interference dot when:
- splittun/usagePolicy has Exclude rules (yellow)
- SPN is active and routes all traffic, fully bypassing Split Tunnel (red)
- SPN is active and partially bypasses Split Tunnel (yellow)

Dot and interference checks are suppressed when the Split Tunneling
or SPN module is globally disabled.
…nel options

- Add validation to Network Interface config to reject whitespace-only values
- Improve "Use Split Tunnel" description to clarify default physical interface detection behavior
…ions

Proxied egress connections from ownPID were still running through
checkTunneling(), causing them to be routed via SPN if Portmaster's
own profile had SPN enabled. Add a checkTunnel flag that is set to
false for isOwnSplitTunnelProxyConnection to preserve the original
app's routing decision.
stenya added 3 commits May 14, 2026 12:12
Proxy split tunnel connections bypassed both filter and tunnel checks,
leaving no code path to trigger the GeoIP lookup, so Country, ASN and
AS Org showed as N/A in the UI.

Add Entity.FetchLocation (GeoIP only, no filter lists) and call it
unconditionally at the start of FilterConnection.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ stenya
❌ Alexandr Stelnykovych


Alexandr Stelnykovych seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements a comprehensive split tunnel feature enabling selective per-connection routing through a local TCP/UDP proxy to alternative network interfaces, alongside configuration management, firewall integration, UI controls, and network interface discovery across multiple platforms.

Changes

Split Tunnel Feature

Layer / File(s) Summary
Verdict and Packet Interface for Split Tunnel
service/network/status.go, service/network/packet/packet.go, service/firewall/interception/nfq/packet.go, service/firewall/interception/packet_tracer.go, service/firewall/interception/windowskext/packet.go, service/firewall/interception/windowskext2/packet.go, desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts, windows_kext/kextinterface/command.go
New VerdictRerouteToSplitTun constant and method signatures implemented across platform packet handlers (Linux nfqueue, Windows kext, traced packets) and API types.
Split Tunnel Module Initialization and Lifecycle
service/splittun/config.go, service/splittun/module.go, service/instance.go
Core SplitTunModule with configuration wiring, atomic enable/disable state machine, proxy lifecycle management, and service integration.
TCP/UDP Proxy Implementation
service/splittun/proxy/common.go, service/splittun/proxy/tcp_proxy.go, service/splittun/proxy/udp_proxy.go, service/splittun/proxy/cache.go
Layer-4 transparent proxies with per-session routing decisions, bidirectional piping, session cache with metrics, buffer pooling, graceful shutdown, and idle timeout handling.
Proxy Lifecycle and Request Handling
service/splittun/proxies.go, service/splittun/requests.go, service/splittun/proxy/bind_linux.go, service/splittun/proxy/bind_other.go
Proxy startup/shutdown coordination, proxied connection detection, TTL-based pending request tracking with cleanup, and Linux SO_BINDTODEVICE interface binding.
Firewall Packet Handling and Split Tunnel Verdict Routing
service/firewall/packet_handler.go, service/firewall/split-tunnel.go
Packet handler integration with split-tunnel eligibility checks, profile management, usage policy enforcement, verdict routing, and own-proxy connection exclusion.
Conntrack Management for Split Tunnel and IPv6
service/firewall/interception/nfq/conntrack.go, service/firewall/interception/nfqueue_linux.go
Conntrack cleanup for unmarked entries, IPv6-aware family selection in marked connection deletion, and iptables/nft rules for split tunnel DNAT.
Split Tunnel Configuration and Profile Management
service/profile/config.go, service/profile/profile-layered.go, service/profile/profile.go, service/profile/config-update.go
Configuration keys for enable/interface/usage-policy, layered profile options with global fallback, and endpoint policy parsing.
Verdict Handling in Connection and Metrics
service/network/connection.go, service/network/api.go, service/network/metrics.go, service/nameserver/nameserver.go, service/netquery/manager.go
Split tunnel context on connections, verdict classification as accepted for metrics/debug, immediate save on reroute verdicts, and extra data enrichment.
Physical Network Interface Discovery and Binding
service/netenv/interfaces.go, service/netenv/interfaces_linux.go, service/netenv/interfaces_windows.go, service/netenv/interfaces_default.go, service/netenv/interfaces_test.go
Cached interface enumeration with routable unicast IP filtering, platform-specific default route detection for IPv4/IPv6, and comprehensive integration tests.
Angular UI Components and Settings
desktop/angular/src/app/app.module.ts, desktop/angular/src/app/pages/app-view/app-view.html, desktop/angular/src/app/pages/app-view/qs-use-splittun/qs-use-splittun.ts, desktop/angular/src/app/pages/app-view/qs-use-splittun/qs-use-splittun.html, desktop/angular/src/app/shared/config/subsystems.ts, desktop/angular/src/app/shared/config/config-settings.ts, desktop/angular/src/app/pages/dashboard/dashboard.component.html
Quick-settings component for split tunnel enable/disable with SPN interference detection, subsystem registration, config scope navigation, and dashboard username display toggle.
Network Query and Connection Data Enrichment
desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts, desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts, desktop/angular/src/app/shared/netquery/connection-details/conn-details.html, desktop/angular/src/app/shared/netquery/netquery.component.ts
Split tunnel context types, extra data population, verdict aggregation in profile stats, and connection detail display with interface/IP.
IVPN WireGuard Compatibility Rules for Split Tunnel
service/interop/ivpn/evt_handlers.go, service/interop/ivpn/hook_default.go, service/interop/ivpn/hook_linux.go, service/interop/ivpn/hook_windows.go, service/interop/ivpn/ivpn.go
Generalized compatibility reconciliation for SPN and split tunnel, refactored WireGuard kill-switch bypass rules, and nft table existence checking.
Runtime Log Level Management
base/log/logging.go, base/log/output.go, base/log/slog.go
Dynamic log level updates via shared slog.LevelVar, one-time default handler initialization, and level-change notifications.
Feature Access Configuration
spn/access/features.go
Split Tunnel feature added to free tier, replacing Safing Support feature.
Angular Build Tooling
packaging/linux/dev_helpers/build_angular.sh
Bash build script for developing and packaging Angular UI with development/production modes and interactive prompts.
Miscellaneous Integration Updates
service/intel/entity.go, service/network/dns.go, service/network/packet/info_only.go, windows_kext/driver/src/packet_callouts.rs
Entity location fetching, DNS handler clarifications, info packet verdict stubs, and Windows kext port constant inclusion.
Windows Kext Interface Updates
windows_kext/kextinterface/version.txt
Version bump to reflect kext interface changes.
Proxy Documentation and Tests
service/splittun/proxy/README.md, service/splittun/proxy/bench_test.go, service/splittun/proxy/proxy_test.go
Comprehensive README with API documentation, usage examples, and design notes; benchmarks and end-to-end tests for TCP/UDP proxy throughput, session creation, and graceful shutdown.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • safing/portmaster#2137: Adds Windows kext split-tunnel redirect verdict and PM_SPLIT_TUN_PORT handling in driver/ALE callouts, overlapping with the split-tunnel verdict plumbing added here.
  • safing/portmaster#2135: Modifies filterHandler tunneling control flow with skipTunnel flag; related to the split-tunnel reroute decision logic in this PR.

Suggested reviewers

  • vlabo
  • dhaavi
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/split-tunneling

@stenya
stenya merged commit 4c2a8f7 into development Jun 11, 2026
4 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants