Files
Alexandr Stelnykovych 2252fd17ed ivpn/linux: route SPN hub traffic around VPN tunnel (split tunnel)
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
2026-03-26 23:20:34 +02:00

135 lines
4.1 KiB
Go

package interop
import (
"errors"
"net"
"sync/atomic"
"github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/service/firewall"
"github.com/safing/portmaster/service/firewall/interception"
"github.com/safing/portmaster/service/interop/ivpn"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/network"
"github.com/safing/portmaster/spn/hub"
)
// Interface for separate interoperability objects.
// Each object can implement interoperability with a specific third-party application,
// such as IVPN client, to exchange information and coordinate actions for better compatibility and user experience.
type interopModule interface {
Start() error
PingHandler() error
VerdictHandler(conn *network.Connection) (network.Verdict, string, bool)
}
type instance interface {
Config() *config.Config
Interception() *interception.Interception
GetHookSPNConnecting() *mgr.HookMgr[hub.Announcement]
}
// Module for interoperability with third-party applications
type Interoperability struct {
mgr *mgr.Manager
instance instance
cfgDnsNameServers config.StringArrayOption
dnsListenAddress string
interopModules []interopModule
verdictHandlerRegistered atomic.Bool
}
// Manager returns the module manager.
func (u *Interoperability) Manager() *mgr.Manager {
return u.mgr
}
// Start starts the module.
func (u *Interoperability) Start() error {
return start()
}
// Stop stops the module.
func (u *Interoperability) Stop() error {
return stop()
}
func (u *Interoperability) DnsListenAddress() string {
return u.dnsListenAddress
}
func (u *Interoperability) DnsNameServers() []string {
return u.cfgDnsNameServers()
}
func (u *Interoperability) EvtConfigChange() <-chan struct{} {
return u.instance.Config().EventConfigChange.Subscribe("interoperability: config change detection", 10).Events()
}
func (u *Interoperability) Interception() *interception.Interception {
return u.instance.Interception()
}
func (u *Interoperability) GetHookSPNConnecting() *mgr.HookMgr[hub.Announcement] {
return u.instance.GetHookSPNConnecting()
}
func start() error {
for _, im := range module.interopModules {
if err := im.Start(); err != nil {
return errors.New("failed to start interoperability module: " + err.Error())
}
}
return nil
}
func stop() error {
return nil
}
var (
module *Interoperability
shimLoaded atomic.Bool
)
func New(instance instance) (*Interoperability, error) {
if !shimLoaded.CompareAndSwap(false, true) {
return nil, errors.New("only one instance allowed")
}
// Determine Portmaster's local DNS resolver listen address for use in IVPN client configuration.
// Ot is ok to check it once at startup, since it can only be changed by restarting Portmaster with a different config.
var err error
dnsListenAddress := config.GetAsString("dns/listenAddress", "")()
dnsListenAddress, _, err = net.SplitHostPort(dnsListenAddress)
if err != nil || net.ParseIP(dnsListenAddress) == nil || dnsListenAddress == "" || dnsListenAddress == "0.0.0.0" {
dnsListenAddress = "127.0.0.1"
}
// Create module instance.
m := mgr.New("Interop")
module = &Interoperability{
mgr: m,
instance: instance,
cfgDnsNameServers: config.GetAsStringArray("dns/nameservers", []string{}),
dnsListenAddress: dnsListenAddress,
interopModules: make([]interopModule, 0, 1),
}
if err := module.prep(); err != nil {
return nil, err
}
return module, nil
}
func (i *Interoperability) prep() error {
i.interopModules = append(i.interopModules, ivpn.NewInteropIvpn(i))
return i.registerAPIEndpoints()
}
// EnsureVerdictHandlerRegistered registers the interoperability module's verdict handler
// with the firewall module if not already registered.
// We do this lazily here only when we need it.
func (i *Interoperability) EnsureVerdictHandlerRegistered() {
if i.verdictHandlerRegistered.CompareAndSwap(false, true) {
firewall.SetExternalVerdictHandler(i.verdict_handler)
}
}