Skip to content

Commit

Permalink
[fix] Connection loss when switching between WIFI and iPhone Hotspot …
Browse files Browse the repository at this point in the history
…with V2Ray Quic

The root cause of the issue is that the route to the V2Ray server is not updated after a network switch. The solution is to update the routing table every time the default gateway changes to ensure the manual route to the V2Ray server remains up-to-date.

ivpn/desktop-app-shadow#146
  • Loading branch information
stenya committed Oct 2, 2023
1 parent f0a26b1 commit 3b1961e
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 35 deletions.
24 changes: 15 additions & 9 deletions daemon/service/service_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (s *Service) Connect(params types.ConnectionParams) (err error) {
params.OpenVpnParameters.Obfs4proxy = obfsproxy.Config{}
}

return s.connectOpenVPN(originalEntryServerInfo, connectionParams, params.ManualDNS, params.Metadata.AntiTracker, params.FirewallOn, params.FirewallOnDuringConnection, params.OpenVpnParameters.Obfs4proxy)
return s.connectOpenVPN(originalEntryServerInfo, connectionParams, params.ManualDNS, params.Metadata.AntiTracker, params.FirewallOn, params.FirewallOnDuringConnection, params.OpenVpnParameters.Obfs4proxy, v2RayWrapper)

} else if vpn.Type(params.VpnType) == vpn.WireGuard {
if len(params.WireGuardParameters.EntryVpnServer.Hosts) < 1 {
Expand Down Expand Up @@ -287,14 +287,14 @@ func (s *Service) Connect(params types.ConnectionParams) (err error) {
params.WireGuardParameters.Mtu)
}

return s.connectWireGuard(originalEntryServerInfo, connectionParams, params.ManualDNS, params.Metadata.AntiTracker, params.FirewallOn, params.FirewallOnDuringConnection)
return s.connectWireGuard(originalEntryServerInfo, connectionParams, params.ManualDNS, params.Metadata.AntiTracker, params.FirewallOn, params.FirewallOnDuringConnection, v2RayWrapper)
}

return fmt.Errorf("unexpected VPN type to connect (%v)", params.VpnType)
}

// connectOpenVPN start OpenVPN connection
func (s *Service) connectOpenVPN(originalEntryServerInfo *svrConnInfo, connectionParams openvpn.ConnectionParams, manualDNS dns.DnsSettings, antiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool, obfsproxyConfig obfsproxy.Config) error {
func (s *Service) connectOpenVPN(originalEntryServerInfo *svrConnInfo, connectionParams openvpn.ConnectionParams, manualDNS dns.DnsSettings, antiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool, obfsproxyConfig obfsproxy.Config, v2rayWrapper *v2r.V2RayWrapper) error {

createVpnObjfunc := func() (vpn.Process, error) {
prefs := s.Preferences()
Expand Down Expand Up @@ -420,11 +420,11 @@ func (s *Service) connectOpenVPN(originalEntryServerInfo *svrConnInfo, connectio
return vpnObj, nil
}

return s.keepConnection(originalEntryServerInfo, createVpnObjfunc, manualDNS, antiTracker, firewallOn, firewallDuringConnection)
return s.keepConnection(originalEntryServerInfo, createVpnObjfunc, manualDNS, antiTracker, firewallOn, firewallDuringConnection, v2rayWrapper)
}

// connectWireGuard start WireGuard connection
func (s *Service) connectWireGuard(originalEntryServerInfo *svrConnInfo, connectionParams wireguard.ConnectionParams, manualDNS dns.DnsSettings, antiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool) error {
func (s *Service) connectWireGuard(originalEntryServerInfo *svrConnInfo, connectionParams wireguard.ConnectionParams, manualDNS dns.DnsSettings, antiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool, v2rayWrapper *v2r.V2RayWrapper) error {
// stop active connection (if exists)
if err := s.Disconnect(); err != nil {
return fmt.Errorf("failed to connect. Unable to stop active connection: %w", err)
Expand Down Expand Up @@ -477,10 +477,10 @@ func (s *Service) connectWireGuard(originalEntryServerInfo *svrConnInfo, connect
return vpnObj, nil
}

return s.keepConnection(originalEntryServerInfo, createVpnObjfunc, manualDNS, antiTracker, firewallOn, firewallDuringConnection)
return s.keepConnection(originalEntryServerInfo, createVpnObjfunc, manualDNS, antiTracker, firewallOn, firewallDuringConnection, v2rayWrapper)
}

func (s *Service) keepConnection(originalEntryServerInfo *svrConnInfo, createVpnObj func() (vpn.Process, error), initialManualDNS dns.DnsSettings, initialAntiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool) (retError error) {
func (s *Service) keepConnection(originalEntryServerInfo *svrConnInfo, createVpnObj func() (vpn.Process, error), initialManualDNS dns.DnsSettings, initialAntiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool, v2rayWrapper *v2r.V2RayWrapper) (retError error) {
prefs := s.Preferences()
if !prefs.Session.IsLoggedIn() {
return srverrors.ErrorNotLoggedIn{}
Expand Down Expand Up @@ -525,7 +525,7 @@ func (s *Service) keepConnection(originalEntryServerInfo *svrConnInfo, createVpn
}

// start connection
connErr := s.connect(originalEntryServerInfo, vpnObj, dns, antitracker, firewallOn, firewallDuringConnection)
connErr := s.connect(originalEntryServerInfo, vpnObj, dns, antitracker, firewallOn, firewallDuringConnection, v2rayWrapper)
if connErr != nil {
log.Error(fmt.Sprintf("Connection error: %s", connErr))
if s._requiredVpnState == Connect {
Expand Down Expand Up @@ -583,7 +583,7 @@ func (s *Service) keepConnection(originalEntryServerInfo *svrConnInfo, createVpn
// We need this info to notify correct data about vpn.CONNECTED state: for V2Ray connection the original parameters are overwriten by local V2Ray proxy params ('127.0.0.1:local_port')
// - Param 'firewallOn' - enable firewall before connection (if true - the parameter 'firewallDuringConnection' will be ignored).
// - Param 'firewallDuringConnection' - enable firewall before connection and disable after disconnection (has effect only if Firewall not enabled before)
func (s *Service) connect(originalEntryServerInfo *svrConnInfo, vpnProc vpn.Process, manualDNS dns.DnsSettings, antiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool) error {
func (s *Service) connect(originalEntryServerInfo *svrConnInfo, vpnProc vpn.Process, manualDNS dns.DnsSettings, antiTracker types.AntiTrackerMetadata, firewallOn bool, firewallDuringConnection bool, v2rayWrapper *v2r.V2RayWrapper) error {
var connectRoutinesWaiter sync.WaitGroup

// stop active connection (if exists)
Expand Down Expand Up @@ -812,6 +812,12 @@ func (s *Service) connect(originalEntryServerInfo *svrConnInfo, vpnProc vpn.Proc
isRuning = false
}
case <-routingUpdateChan: // there were some routing changes but 'interfaceToProtect' is still is the default route
// If V2Ray is in use - we must update route to V2Ray server each time when default gateway IP was chnaged
if v2rayWrapper != nil {
if err := v2rayWrapper.UpdateMainRoute(); err != nil {
log.Error(err)
}
}
s._vpn.OnRoutingChanged()
go func() {
// Ensure that current DNS configuration is correct. If not - it re-apply the required configuration.
Expand Down
49 changes: 47 additions & 2 deletions daemon/v2r/v2ray.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"time"

"github.com/ivpn/desktop-app/daemon/logger"
"github.com/ivpn/desktop-app/daemon/netinfo"
"github.com/ivpn/desktop-app/daemon/shell"
)

Expand Down Expand Up @@ -73,6 +74,9 @@ type V2RayWrapper struct {
command *exec.Cmd
mutex sync.Mutex
stoppedChan chan struct{}

// IP address of the default gateway which was used for static route to V2Ray server
defaultGeteway net.IP
}

// CreateV2RayWrapper - creates new V2RayWrapper object
Expand Down Expand Up @@ -147,6 +151,47 @@ func (v *V2RayWrapper) Start() error {
return v.start()
}

// UpdateMainRoute - updates the route to V2Ray server.
// This method must be called when the default route was changed (e.g. chnaged WiFi network)
func (v *V2RayWrapper) UpdateMainRoute() error {
v.mutex.Lock()
defer v.mutex.Unlock()

gwIp, err := netinfo.DefaultGatewayIP()
if err != nil {
return fmt.Errorf("getting default gateway ip error : %w", err)
}

if v.defaultGeteway != nil && v.defaultGeteway.Equal(gwIp) {
return nil //gateway did not chnage. Nothing to update
}

log.Info("Updating route to V2Ray server...")
if err := v.deleteMainRoute(); err != nil {
log.Error(err)
}
return v.setMainRoute(gwIp)
}

func (v *V2RayWrapper) setMainRoute(defaultGateway net.IP) error {
var err error
if defaultGateway == nil {
defaultGateway, err = netinfo.DefaultGatewayIP()
if err != nil {
return fmt.Errorf("getting default gateway ip error : %w", err)
}
}
if err := v.implSetMainRoute(defaultGateway); err == nil {
v.defaultGeteway = defaultGateway
}
return err
}

func (v *V2RayWrapper) deleteMainRoute() error {
v.defaultGeteway = nil
return v.implDeleteMainRoute()
}

func (v *V2RayWrapper) start() (retError error) {
// check if object correctly initialized
if v.binary == "" {
Expand Down Expand Up @@ -183,13 +228,13 @@ func (v *V2RayWrapper) start() (retError error) {
}

// Apply route to remote endpoint
if err := v.implSetMainRoute(); err != nil {
if err := v.setMainRoute(nil); err != nil {
return fmt.Errorf("error applying route to remote V2Ray endpoint: %w", err)
}
defer func() {
if retError != nil {
// in case of error - ensure route is deleted
v.implDeleteMainRoute()
v.deleteMainRoute()
}
}()

Expand Down
11 changes: 3 additions & 8 deletions daemon/v2r/v2ray_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,23 @@ package v2r

import (
"fmt"
"net"

"github.com/ivpn/desktop-app/daemon/netinfo"
"github.com/ivpn/desktop-app/daemon/shell"
)

func implInit() {
// nothing to do here for macOS
}

func (v *V2RayWrapper) implSetMainRoute() error {
gwIp, err := netinfo.DefaultGatewayIP()
if err != nil {
return fmt.Errorf("getting default gateway ip error : %w", err)
}

func (v *V2RayWrapper) implSetMainRoute(defaultGateway net.IP) error {
remoteHost, _, err := v.getRemoteEndpoint()
if err != nil {
return fmt.Errorf("getting remote endpoint error : %w", err)
}

// ip route add 144.217.233.114/32 via 192.168.0.1 dev eth0
if err := shell.Exec(log, "/sbin/route", "-n", "add", "-inet", "-net", remoteHost.String(), gwIp.String(), "255.255.255.255"); err != nil {
if err := shell.Exec(log, "/sbin/route", "-n", "add", "-inet", "-net", remoteHost.String(), defaultGateway.String(), "255.255.255.255"); err != nil {
return fmt.Errorf("adding route shell comand error : %w", err)
}

Expand Down
11 changes: 3 additions & 8 deletions daemon/v2r/v2ray_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,23 @@ package v2r

import (
"fmt"
"net"

"github.com/ivpn/desktop-app/daemon/netinfo"
"github.com/ivpn/desktop-app/daemon/shell"
)

func implInit() {
// nothing to do here for macOS
}

func (v *V2RayWrapper) implSetMainRoute() error {
gwIp, err := netinfo.DefaultGatewayIP()
if err != nil {
return fmt.Errorf("getting default gateway ip error : %w", err)
}

func (v *V2RayWrapper) implSetMainRoute(defaultGateway net.IP) error {
remoteHost, _, err := v.getRemoteEndpoint()
if err != nil {
return fmt.Errorf("getting remote endpoint error : %w", err)
}

// /sbin/ip route add 144.217.148.72/32 via 192.168.2.1
if err := shell.Exec(log, "ip", "route", "add", remoteHost.String()+"/32", "via", gwIp.String()); err != nil {
if err := shell.Exec(log, "ip", "route", "add", remoteHost.String()+"/32", "via", defaultGateway.String()); err != nil {
return fmt.Errorf("adding route shell comand error : %w", err)
}

Expand Down
11 changes: 3 additions & 8 deletions daemon/v2r/v2ray_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ package v2r

import (
"fmt"
"net"
"os"
"path"
"strings"

"github.com/ivpn/desktop-app/daemon/netinfo"
"github.com/ivpn/desktop-app/daemon/shell"
)

Expand All @@ -43,23 +43,18 @@ func implInit() {
}
}

func (v *V2RayWrapper) implSetMainRoute() error {
func (v *V2RayWrapper) implSetMainRoute(defaultGateway net.IP) error {
if routeBinaryPath == "" {
return fmt.Errorf("route.exe location not specified")
}

gwIp, err := netinfo.DefaultGatewayIP()
if err != nil {
return fmt.Errorf("getting default gateway ip error : %w", err)
}

remoteHost, _, err := v.getRemoteEndpoint()
if err != nil {
return fmt.Errorf("getting remote endpoint error : %w", err)
}

// route.exe add 144.217.233.114 mask 255.255.255.255 192.168.0.1
if err := shell.Exec(log, routeBinaryPath, "add", remoteHost.String(), "mask", "255.255.255.255", gwIp.String()); err != nil {
if err := shell.Exec(log, routeBinaryPath, "add", remoteHost.String(), "mask", "255.255.255.255", defaultGateway.String()); err != nil {
return fmt.Errorf("adding route shell comand error : %w", err)
}

Expand Down

0 comments on commit 3b1961e

Please sign in to comment.