interop/ivpn: suppress network-derived location while VPN is active

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
This commit is contained in:
Alexandr Stelnykovych
2026-03-27 17:00:19 +02:00
parent e0fc06aa49
commit b2805d35ae
3 changed files with 35 additions and 0 deletions
+9
View File
@@ -7,11 +7,17 @@ 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/service/network/packet"
)
// notification handler: VPN connection is going to start
func (i *InteropIvpn) onConnectionStarting(wc *mgr.WorkerCtx, _ string, messageData string) {
// While VPN is active, ignore network-derived location sources.
// Virtual VPN interfaces/IPs can skew detected device location
// and lead to selecting an incorrect SPN home hub.
netenv.DisableNetworkDerivedLocation(true)
connInfo := ivpnclient.ConnectionStarting{}
err := json.Unmarshal([]byte(messageData), &connInfo)
if err != nil {
@@ -38,6 +44,9 @@ func (i *InteropIvpn) onConnectionStarting(wc *mgr.WorkerCtx, _ string, messageD
// notification handler: VPN connection stopped
func (i *InteropIvpn) onConnectionStopped(wc *mgr.WorkerCtx, _ string, _ string) {
// Re-enable network-derived location methods now that VPN is inactive.
netenv.DisableNetworkDerivedLocation(false)
status := *i.getStatus()
status.vpnConnection = vpnConnectionInfo{}
status.connectedInfo = nil
+4
View File
@@ -12,6 +12,7 @@ import (
"github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service/firewall/interception"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/spn/hub"
@@ -115,6 +116,9 @@ var notifWarnOldVersion atomic.Pointer[notifications.Notification]
// Synchronously connects to the IVPN client, sets up message handlers
func (i *InteropIvpn) connectIvpnClient(wc *mgr.WorkerCtx) error {
defer func() {
// Re-enable network-derived location methods when disconnecting from IVPN client, since VPN is no longer active.
netenv.DisableNetworkDerivedLocation(false)
// Clear client status on disconnect
i.setStatus(nil)
// Reset DNS tracking state
+22
View File
@@ -6,6 +6,7 @@ import (
"net"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/google/gopacket/layers"
@@ -33,6 +34,12 @@ var (
locationsLock sync.Mutex
gettingLocationsLock sync.Mutex
locationNetworkChangedFlag = GetNetworkChangedFlag()
// disableNetworkDerivedLocation disables location methods that depend on network
// configuration, such as reading interface IPs or using traceroute.
// Use this when a VPN is known to be active and network-derived location would
// reflect VPN egress rather than the device's physical uplink.
disableNetworkDerivedLocation atomic.Bool
)
func prepLocation() (err error) {
@@ -40,6 +47,13 @@ func prepLocation() (err error) {
return err
}
// DisableNetworkDerivedLocation disables or enables location methods that depend on network
// configuration, such as reading interface IPs or using traceroute. Use this when a VPN is known to be active and
// network-derived location would reflect VPN egress rather than the device's physical uplink.
func DisableNetworkDerivedLocation(disable bool) {
disableNetworkDerivedLocation.Store(disable)
}
// DeviceLocations holds multiple device locations.
type DeviceLocations struct {
All []*DeviceLocation
@@ -332,6 +346,10 @@ func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
}
func getLocationFromInterfaces(dls *DeviceLocations) (v4ok, v6ok bool) {
if disableNetworkDerivedLocation.Load() {
return false, false
}
globalIPv4, globalIPv6, err := GetAssignedGlobalAddresses()
if err != nil {
log.Warningf("netenv: location: failed to get assigned global addresses: %s", err)
@@ -362,6 +380,10 @@ func getLocationFromUPnP() (ok bool) {
*/
func getLocationFromTraceroute(dls *DeviceLocations) (dl *DeviceLocation, err error) {
if disableNetworkDerivedLocation.Load() {
return nil, fmt.Errorf("skipped network-derived location")
}
// Create connection.
conn, err := icmp.ListenPacket("ip4:icmp", "")
if err != nil {