mirror of
https://github.com/safing/portmaster.git
synced 2026-05-20 20:40:36 +00:00
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
This commit is contained in:
@@ -18,14 +18,14 @@ import (
|
||||
// See TODO on packet.mark() on their relevance
|
||||
// and a possibility to remove most IPtables rules.
|
||||
const (
|
||||
MarkAccept = 1700
|
||||
MarkBlock = 1701
|
||||
MarkDrop = 1702
|
||||
MarkAcceptAlways = 1710
|
||||
MarkBlockAlways = 1711
|
||||
MarkDropAlways = 1712
|
||||
MarkRerouteNS = 1799
|
||||
MarkRerouteSPN = 1717
|
||||
MarkAccept = 1700 // 0x6a4
|
||||
MarkBlock = 1701 // 0x6a5
|
||||
MarkDrop = 1702 // 0x6a6
|
||||
MarkAcceptAlways = 1710 // 0x6ae
|
||||
MarkBlockAlways = 1711 // 0x6af
|
||||
MarkDropAlways = 1712 // 0x6b0
|
||||
MarkRerouteNS = 1799 // 0x707
|
||||
MarkRerouteSPN = 1717 // 0x6b5
|
||||
)
|
||||
|
||||
func markToString(mark int) string {
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
"github.com/safing/portmaster/spn/captain"
|
||||
"github.com/safing/portmaster/spn/crew"
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
"github.com/safing/portmaster/spn/navigator"
|
||||
"github.com/safing/portmaster/spn/patrol"
|
||||
"github.com/safing/portmaster/spn/ships"
|
||||
@@ -644,6 +645,10 @@ func (i *Instance) GetEventSPNConnected() *mgr.EventMgr[struct{}] {
|
||||
return i.captain.EventSPNConnected
|
||||
}
|
||||
|
||||
func (i *Instance) GetHookSPNConnecting() *mgr.HookMgr[hub.Announcement] {
|
||||
return i.captain.HookSPNConnecting
|
||||
}
|
||||
|
||||
// Special functions
|
||||
|
||||
// SetCmdLineOperation sets a command line operation to be executed instead of starting the system. This is useful when functions need all modules to be prepared for a special operation.
|
||||
|
||||
@@ -44,6 +44,8 @@ func (i *InteropIvpn) onConnectionStopped(wc *mgr.WorkerCtx, _ string, _ string)
|
||||
i.setStatus(&status)
|
||||
|
||||
wc.Debug("IVPN: VPN connection stopped")
|
||||
|
||||
_ = i.ensureSPNCompatibility(wc)
|
||||
}
|
||||
|
||||
// notification handler: VPN connection established successfully
|
||||
@@ -62,8 +64,5 @@ func (i *InteropIvpn) onConnectedResp(wc *mgr.WorkerCtx, _ string, messageData s
|
||||
wc.Debug(fmt.Sprintf("IVPN: VPN connection established (vpnType:%v; localIPv4:%v; localIPv6:%v)",
|
||||
connectedResp.VpnType, connectedResp.ClientIP, connectedResp.ClientIPv6))
|
||||
|
||||
err = i.ensureSPNCompatibility(wc)
|
||||
if err != nil {
|
||||
wc.Warn(fmt.Sprintf("IVPN: %v", err))
|
||||
}
|
||||
_ = i.ensureSPNCompatibility(wc)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ package ivpn
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
type platformSpecific struct{}
|
||||
|
||||
func (i *InteropIvpn) spnConnectingHook(w *mgr.WorkerCtx, hookCtx hub.Announcement) (cancel bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
func (i *InteropIvpn) ensureSPNCompatibility(wc *mgr.WorkerCtx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,22 +12,38 @@ import (
|
||||
|
||||
"github.com/ivpn/desktop-app/daemon/protocol/ivpnclient"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
type platformSpecific struct {
|
||||
spnWgNftRuleHandle atomic.Int32 // nft rule handle we registered for SPN compatibility with WireGuard
|
||||
spnWgIptRuleIP atomic.Value // last WG local IP used for iptables fallback rule (string)
|
||||
spnWgNftRuleHandle atomic.Int32 // nft rule handle we registered for SPN compatibility with WireGuard
|
||||
spnWgIptRuleIP atomic.Value // last WG local IP used for iptables fallback rule (string)
|
||||
spnHubInfo atomic.Pointer[hub.Announcement] // last SPN hub info (hub.Info)
|
||||
}
|
||||
|
||||
const (
|
||||
// NOTE: The nft table name is currently tied to IVPN's wg-quick setup.
|
||||
// If IVPN changes the WG interface naming, this constant may need adjustment.
|
||||
nftTableWgQuickIvpn = "wg-quick-wgivpn"
|
||||
nftRuleCommentSPNCompat = "portmaster-spn-lo-rnat"
|
||||
spnSlitTunRouteTableID = "717"
|
||||
spnSlitTunRulePriority = "717"
|
||||
)
|
||||
|
||||
func (i *InteropIvpn) spnConnectingHook(wc *mgr.WorkerCtx, homeHub hub.Announcement) (cancel bool, err error) {
|
||||
return false, i.ensureSpnHubBypassVpnRoutes(wc, &homeHub)
|
||||
}
|
||||
|
||||
func (i *InteropIvpn) ensureSPNCompatibility(wc *mgr.WorkerCtx) error {
|
||||
err := i.reconcileWgCompatRule(wc)
|
||||
err := i.ensureWgSpnCompatRule(wc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reconcile WireGuard compatibility rule: %w", err)
|
||||
wc.Warn(fmt.Sprintf("IVPN: failed to ensure WireGuard compatibility rule: %v", err))
|
||||
}
|
||||
|
||||
err = i.ensureSpnHubBypassVpnRoutes(wc, i.extra.spnHubInfo.Load())
|
||||
if err != nil {
|
||||
wc.Warn(fmt.Sprintf("IVPN: failed to ensure VPN and SPN tunnel routes: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -50,10 +66,7 @@ func (i *InteropIvpn) ensureSPNCompatibility(wc *mgr.WorkerCtx) error {
|
||||
// Rule lifecycle is managed here:
|
||||
// - Remove previously managed rule (nft/iptables) first.
|
||||
// - Recreate only when WireGuard is connected and SPN is enabled.
|
||||
//
|
||||
// NOTE: The nft table/chain name is currently tied to IVPN's wg-quick setup.
|
||||
// If IVPN changes the WG interface naming, this constant may need adjustment.
|
||||
func (i *InteropIvpn) reconcileWgCompatRule(wc *mgr.WorkerCtx) error {
|
||||
func (i *InteropIvpn) ensureWgSpnCompatRule(wc *mgr.WorkerCtx) error {
|
||||
status := i.getStatus()
|
||||
connectedInfo := status.connectedInfo
|
||||
|
||||
@@ -168,3 +181,159 @@ func parseNftInsertHandle(data []byte) (int, error) {
|
||||
}
|
||||
return 0, fmt.Errorf("no rule entry found in nft output")
|
||||
}
|
||||
|
||||
// ensureSpnHubBypassVpnRoutes keeps Linux policy routing in sync so
|
||||
// traffic to the selected SPN hub is sent via the system default gateway, not
|
||||
// through the active VPN tunnel.
|
||||
//
|
||||
// Why this is needed:
|
||||
// - When IVPN is connected, default routing points to the VPN interface.
|
||||
// - SPN hub control/data path must reach the hub directly on the non-VPN uplink.
|
||||
// - Without this rule/table setup, SPN hub traffic can be tunneled into VPN
|
||||
//
|
||||
// The function removes stale rules/routes from previous hub state, installs a
|
||||
// dedicated routing table default route via the non-VPN gateway, and adds a
|
||||
// high-priority destination rule for the current hub IP.
|
||||
func (i *InteropIvpn) ensureSpnHubBypassVpnRoutes(wc *mgr.WorkerCtx, hubInfo *hub.Announcement) error {
|
||||
oldHubInfo := i.extra.spnHubInfo.Swap(hubInfo)
|
||||
|
||||
ipPath, _ := exec.LookPath("ip")
|
||||
if ipPath == "" {
|
||||
return fmt.Errorf("failed to ensure VPN and SPN tunnel routes: ip command not found")
|
||||
}
|
||||
|
||||
deleteRule := func(family, destination string) {
|
||||
if destination == "" {
|
||||
return
|
||||
}
|
||||
_ = exec.Command(ipPath, family, "rule", "del", "pref", spnSlitTunRulePriority,
|
||||
"to", destination, "lookup", spnSlitTunRouteTableID).Run()
|
||||
}
|
||||
|
||||
// Clean up old rules for previous hub destination (if any).
|
||||
if oldHubInfo != nil {
|
||||
if oldHubInfo.IPv4 != nil {
|
||||
deleteRule("-4", oldHubInfo.IPv4.String()+"/32")
|
||||
}
|
||||
if oldHubInfo.IPv6 != nil {
|
||||
deleteRule("-6", oldHubInfo.IPv6.String()+"/128")
|
||||
}
|
||||
_ = exec.Command(ipPath, "-4", "route", "flush", "table", spnSlitTunRouteTableID).Run()
|
||||
_ = exec.Command(ipPath, "-6", "route", "flush", "table", spnSlitTunRouteTableID).Run()
|
||||
}
|
||||
|
||||
// If VPN is not connected - we do not need to set up the rules.
|
||||
connectedInfo := i.getStatus().connectedInfo
|
||||
if connectedInfo == nil || connectedInfo.IsPaused {
|
||||
return nil
|
||||
}
|
||||
vpnInterfaceIP := net.ParseIP(connectedInfo.ClientIP)
|
||||
|
||||
// If SPN not enabled - we do not need the rule
|
||||
// And erase stale info about the spnHub
|
||||
if !i.cfgSpnEnabled() || hubInfo == nil {
|
||||
i.extra.spnHubInfo.Store(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check the default gateway:
|
||||
// - the only one default gateway must be present
|
||||
// - the VPN connection gateway (interface) must be ignored
|
||||
var gw *netenv.GatewayInfo = nil
|
||||
gateways := netenv.GatewaysInfo()
|
||||
for _, g := range gateways {
|
||||
if g.Mask == nil || g.IP == nil || g.Interface == "" {
|
||||
continue
|
||||
}
|
||||
// Mask: /0 - candidate default gateway.
|
||||
if ones, _ := g.Mask.Size(); ones == 0 {
|
||||
// Skip the gateway if it belongs to the VPN tunnel interface (heuristic by IP).
|
||||
if has, err := hasInterfaceIp(g.Interface, vpnInterfaceIP); err == nil && has {
|
||||
continue
|
||||
}
|
||||
// in case more than 1 default gateway exists, we can not be sure which one is correct
|
||||
if gw != nil {
|
||||
return nil
|
||||
}
|
||||
gw = &g
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize route table
|
||||
|
||||
family := "-4"
|
||||
hubRule := ""
|
||||
if gw.IP.To4() != nil {
|
||||
if err := exec.Command(ipPath, "-4", "route", "replace", "default",
|
||||
"via", gw.IP.String(), "dev", gw.Interface, "table", spnSlitTunRouteTableID).Run(); err != nil {
|
||||
return fmt.Errorf("failed to set IPv4 default route for SPN slit tunnel table: %w", err)
|
||||
}
|
||||
|
||||
if hubInfo.IPv4 != nil {
|
||||
hubRule = hubInfo.IPv4.String() + "/32"
|
||||
}
|
||||
} else {
|
||||
family = "-6"
|
||||
if err := exec.Command(ipPath, "-6", "route", "replace", "default",
|
||||
"via", gw.IP.String(), "dev", gw.Interface, "table", spnSlitTunRouteTableID).Run(); err != nil {
|
||||
return fmt.Errorf("failed to set IPv6 default route for SPN slit tunnel table: %w", err)
|
||||
}
|
||||
|
||||
if hubInfo.IPv6 != nil {
|
||||
hubRule = hubInfo.IPv6.String() + "/128"
|
||||
}
|
||||
}
|
||||
|
||||
if hubRule == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove potential stale rule for current hub destination before adding.
|
||||
deleteRule(family, hubRule)
|
||||
|
||||
// Initialize rule to route SPN traffic
|
||||
if err := exec.Command(
|
||||
ipPath,
|
||||
family,
|
||||
"rule", "add",
|
||||
"pref", spnSlitTunRulePriority,
|
||||
"to", hubRule,
|
||||
"lookup", spnSlitTunRouteTableID,
|
||||
).Run(); err != nil {
|
||||
return fmt.Errorf("failed to add SPN hub policy route rule (%s): %w", hubRule, err)
|
||||
}
|
||||
|
||||
wc.Debug(fmt.Sprintf("IVPN: Reconciled SPN hub route rule (%s -> table %s via %s dev %s)", hubRule, spnSlitTunRouteTableID, gw.IP.String(), gw.Interface))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasInterfaceIp checks if the given IP address is assigned to the specified network interface.
|
||||
func hasInterfaceIp(ifName string, ip net.IP) (bool, error) {
|
||||
iface, err := net.InterfaceByName(ifName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var currentIP net.IP
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
currentIP = v.IP
|
||||
case *net.IPAddr:
|
||||
currentIP = v.IP
|
||||
}
|
||||
|
||||
if currentIP != nil && currentIP.Equal(ip) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
const DNS_LOCKED_DESCRIPTION = "Portmaster controls all DNS resolution on this system, which prevents conflicts with IVPN's DNS settings. To let IVPN manage DNS again, remove all DNS servers from Portmaster's DNS configuration."
|
||||
@@ -25,6 +26,7 @@ type interopBase interface {
|
||||
DnsListenAddress() string
|
||||
DnsNameServers() []string
|
||||
EvtConfigChange() <-chan struct{}
|
||||
GetHookSPNConnecting() *mgr.HookMgr[hub.Announcement]
|
||||
Interception() *interception.Interception
|
||||
Manager() *mgr.Manager
|
||||
}
|
||||
@@ -79,6 +81,10 @@ func (i *InteropIvpn) setStatus(status *clientStatus) {
|
||||
func (i *InteropIvpn) Start() error {
|
||||
i.connHandler = i.owner.Manager().NewWorkerMgr("ivpn client interoperability", i.connectIvpnClient, nil)
|
||||
|
||||
// Subscribe to SPN connecting hook to ensure SPN compatibility rules
|
||||
// are applied before SPN connects to the network.
|
||||
i.owner.GetHookSPNConnecting().AddHook("ivpn", i.spnConnectingHook)
|
||||
|
||||
firstTryChan := make(chan struct{})
|
||||
i.firstTryDone.Store(&firstTryChan)
|
||||
|
||||
@@ -168,7 +174,7 @@ func (i *InteropIvpn) connectIvpnClient(wc *mgr.WorkerCtx) error {
|
||||
}
|
||||
|
||||
if helloResp.ServiceBinary == "" {
|
||||
// IVPN client version > v3.15.0 must provide the service binary path in the hello response.
|
||||
// IVPN client version > v3.15.1 must provide the service binary path in the hello response.
|
||||
wc.Warn(fmt.Sprintf("Detected IVPN Client version '%v' is incompatible. The hello response did not include all required fields.", helloResp.Version))
|
||||
notif := i.showNotificationWarnOldVersion()
|
||||
notifWarnOldVersion.Store(notif)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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.
|
||||
@@ -25,6 +26,7 @@ type interopModule interface {
|
||||
type instance interface {
|
||||
Config() *config.Config
|
||||
Interception() *interception.Interception
|
||||
GetHookSPNConnecting() *mgr.HookMgr[hub.Announcement]
|
||||
}
|
||||
|
||||
// Module for interoperability with third-party applications
|
||||
@@ -64,6 +66,9 @@ func (u *Interoperability) EvtConfigChange() <-chan struct{} {
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HookMgr manages synchronous hooks.
|
||||
// Unlike EventMgr, Invoke blocks until every registered hook has returned,
|
||||
// making it suitable for pre-operation hooks where the caller must wait for all
|
||||
// hooks to complete.
|
||||
type HookMgr[T any] struct {
|
||||
name string
|
||||
mgr *Manager
|
||||
lock sync.RWMutex
|
||||
|
||||
callbacks []*EventCallback[T]
|
||||
}
|
||||
|
||||
// NewHookMgr returns a new hook manager.
|
||||
func NewHookMgr[T any](name string, mgr *Manager) *HookMgr[T] {
|
||||
return &HookMgr[T]{
|
||||
name: name,
|
||||
mgr: mgr,
|
||||
}
|
||||
}
|
||||
|
||||
// AddHook registers a hook that will be called synchronously by Invoke.
|
||||
// Use the same EventCallbackFunc signature as EventMgr:
|
||||
// - returning cancel=true removes the hook from future invocations.
|
||||
// - returning a non-nil error causes Invoke to stop and return that error.
|
||||
func (cm *HookMgr[T]) AddHook(hookName string, hook EventCallbackFunc[T]) {
|
||||
cm.lock.Lock()
|
||||
defer cm.lock.Unlock()
|
||||
|
||||
cm.callbacks = append(cm.callbacks, &EventCallback[T]{
|
||||
name: hookName,
|
||||
callback: hook,
|
||||
})
|
||||
}
|
||||
|
||||
// Invoke calls all registered hooks synchronously in registration order.
|
||||
// It blocks until every hook has returned.
|
||||
// If a hook returns an error, Invoke stops immediately and returns that error.
|
||||
// Hooks that return cancel=true are removed from future invocations.
|
||||
func (cm *HookMgr[T]) Invoke(wc *WorkerCtx, data T) error {
|
||||
cm.lock.RLock()
|
||||
snapshot := make([]*EventCallback[T], len(cm.callbacks))
|
||||
copy(snapshot, cm.callbacks)
|
||||
cm.lock.RUnlock()
|
||||
|
||||
var anyCanceled bool
|
||||
for _, ec := range snapshot {
|
||||
if ec.canceled.Load() {
|
||||
anyCanceled = true
|
||||
continue
|
||||
}
|
||||
|
||||
cancel, err := ec.callback(wc, data)
|
||||
if err != nil {
|
||||
if cm.mgr != nil {
|
||||
cm.mgr.Warn(
|
||||
"hook failed",
|
||||
"hook_mgr", cm.name,
|
||||
"hook", ec.name,
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if cancel {
|
||||
ec.canceled.Store(true)
|
||||
anyCanceled = true
|
||||
}
|
||||
}
|
||||
|
||||
if anyCanceled {
|
||||
cm.lock.Lock()
|
||||
defer cm.lock.Unlock()
|
||||
cm.callbacks = slices.DeleteFunc(cm.callbacks, func(ec *EventCallback[T]) bool {
|
||||
return ec.canceled.Load()
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
|
||||
var (
|
||||
gateways = make([]net.IP, 0)
|
||||
gatewaysInfo = make([]GatewayInfo, 0) // same as gateways but with additional info
|
||||
gatewaysLock sync.Mutex
|
||||
gatewaysNetworkChangedFlag = GetNetworkChangedFlag()
|
||||
|
||||
@@ -24,24 +26,47 @@ var (
|
||||
nameserversNetworkChangedFlag = GetNetworkChangedFlag()
|
||||
)
|
||||
|
||||
type GatewayInfo struct {
|
||||
IP net.IP
|
||||
Interface string
|
||||
Mask net.IPMask
|
||||
}
|
||||
|
||||
// Gateways returns the currently active gateways.
|
||||
func Gateways() []net.IP {
|
||||
gatewaysLock.Lock()
|
||||
defer gatewaysLock.Unlock()
|
||||
// Check if the network changed, if not, return cache.
|
||||
refreshGatewaysCache()
|
||||
|
||||
return gateways
|
||||
}
|
||||
|
||||
// GatewaysInfo returns the currently active gateways with interface metadata.
|
||||
func GatewaysInfo() []GatewayInfo {
|
||||
gatewaysLock.Lock()
|
||||
defer gatewaysLock.Unlock()
|
||||
|
||||
refreshGatewaysCache()
|
||||
|
||||
return gatewaysInfo
|
||||
}
|
||||
|
||||
func refreshGatewaysCache() {
|
||||
// Check if the network changed, if not, keep cache.
|
||||
if !gatewaysNetworkChangedFlag.IsSet() {
|
||||
return gateways
|
||||
return
|
||||
}
|
||||
gatewaysNetworkChangedFlag.Refresh()
|
||||
|
||||
gateways = make([]net.IP, 0)
|
||||
gatewaysInfo = make([]GatewayInfo, 0)
|
||||
var decoded []byte
|
||||
|
||||
// open file
|
||||
route, err := os.Open("/proc/net/route")
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not read /proc/net/route: %s", err)
|
||||
return gateways
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = route.Close()
|
||||
@@ -53,10 +78,11 @@ func Gateways() []net.IP {
|
||||
|
||||
// parse
|
||||
for scanner.Scan() {
|
||||
line := strings.SplitN(scanner.Text(), "\t", 4)
|
||||
line := strings.Fields(scanner.Text())
|
||||
if len(line) < 4 {
|
||||
continue
|
||||
}
|
||||
iface := line[0]
|
||||
if line[1] == "00000000" {
|
||||
decoded, err = hex.DecodeString(line[2])
|
||||
if err != nil {
|
||||
@@ -68,7 +94,17 @@ func Gateways() []net.IP {
|
||||
continue
|
||||
}
|
||||
gate := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0])
|
||||
mask := net.IPv4Mask(0, 0, 0, 0)
|
||||
if len(line) > 7 {
|
||||
decodedMask, decodeMaskErr := hex.DecodeString(line[7])
|
||||
if decodeMaskErr != nil {
|
||||
log.Warningf("environment: could not parse netmask %s from /proc/net/route: %s", line[7], decodeMaskErr)
|
||||
} else if len(decodedMask) == 4 {
|
||||
mask = net.IPv4Mask(decodedMask[3], decodedMask[2], decodedMask[1], decodedMask[0])
|
||||
}
|
||||
}
|
||||
gateways = append(gateways, gate)
|
||||
gatewaysInfo = append(gatewaysInfo, GatewayInfo{IP: gate, Interface: iface, Mask: mask})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +112,7 @@ func Gateways() []net.IP {
|
||||
v6route, err := os.Open("/proc/net/ipv6_route")
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not read /proc/net/ipv6_route: %s", err)
|
||||
return gateways
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = v6route.Close()
|
||||
@@ -88,11 +124,17 @@ func Gateways() []net.IP {
|
||||
|
||||
// parse
|
||||
for scanner.Scan() {
|
||||
line := strings.SplitN(scanner.Text(), " ", 6)
|
||||
line := strings.Fields(scanner.Text())
|
||||
if len(line) < 6 {
|
||||
continue
|
||||
}
|
||||
iface := line[len(line)-1]
|
||||
if line[0] == "00000000000000000000000000000000" && line[4] != "00000000000000000000000000000000" {
|
||||
mask := net.CIDRMask(0, 128)
|
||||
prefixLength, parsePrefixLengthErr := strconv.ParseInt(line[1], 16, 32)
|
||||
if parsePrefixLengthErr == nil {
|
||||
mask = net.CIDRMask(int(prefixLength), 128)
|
||||
}
|
||||
decoded, err := hex.DecodeString(line[4])
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not parse gateway %s from /proc/net/ipv6_route: %s", line[2], err)
|
||||
@@ -104,10 +146,9 @@ func Gateways() []net.IP {
|
||||
}
|
||||
gate := net.IP(decoded)
|
||||
gateways = append(gateways, gate)
|
||||
gatewaysInfo = append(gatewaysInfo, GatewayInfo{IP: gate, Interface: iface, Mask: mask})
|
||||
}
|
||||
}
|
||||
|
||||
return gateways
|
||||
}
|
||||
|
||||
// Nameservers returns the currently active nameservers.
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
"github.com/safing/portmaster/spn/crew"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
"github.com/safing/portmaster/spn/navigator"
|
||||
"github.com/safing/portmaster/spn/patrol"
|
||||
"github.com/safing/portmaster/spn/ships"
|
||||
@@ -26,6 +27,9 @@ import (
|
||||
// SPNConnectedEvent is the name of the event that is fired when the SPN has connected and is ready.
|
||||
const SPNConnectedEvent = "spn connect"
|
||||
|
||||
// SPNConnectingHook is the name of the hook that is invoked synchronously before the SPN connects to a remote hub.
|
||||
const SPNConnectingHook = "spn connecting"
|
||||
|
||||
// Captain is the main module of the SPN.
|
||||
type Captain struct {
|
||||
mgr *mgr.Manager
|
||||
@@ -38,6 +42,7 @@ type Captain struct {
|
||||
|
||||
states *mgr.StateMgr
|
||||
EventSPNConnected *mgr.EventMgr[struct{}]
|
||||
HookSPNConnecting *mgr.HookMgr[hub.Announcement] // Called before SPN connects to a remote hub
|
||||
}
|
||||
|
||||
// Manager returns the module manager.
|
||||
@@ -237,6 +242,7 @@ func New(instance instance) (*Captain, error) {
|
||||
|
||||
states: mgr.NewStateMgr(m),
|
||||
EventSPNConnected: mgr.NewEventMgr[struct{}](SPNConnectedEvent, m),
|
||||
HookSPNConnecting: mgr.NewHookMgr[hub.Announcement](SPNConnectingHook, m),
|
||||
|
||||
publicIdentityUpdater: m.NewWorkerMgr("maintain public identity", maintainPublicIdentity, nil),
|
||||
statusUpdater: m.NewWorkerMgr("maintain public status", maintainPublicStatus, nil),
|
||||
|
||||
@@ -139,6 +139,13 @@ func connectToHomeHub(wCtx *mgr.WorkerCtx, dst *hub.Hub) error {
|
||||
ctx, cancel := context.WithTimeout(wCtx.Ctx(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Invoke hooks (if any) before opening connection
|
||||
if dst.Info != nil {
|
||||
if err := module.HookSPNConnecting.Invoke(wCtx, *dst.Info); err != nil {
|
||||
return fmt.Errorf("pre-connect hook rejected: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set and clean up exceptions.
|
||||
setExceptions(dst.Info.IPv4, dst.Info.IPv6)
|
||||
defer setExceptions(nil, nil)
|
||||
|
||||
Reference in New Issue
Block a user