From 1fd1f6fcb4b598aeccabc47ff95b0fcd4d952f45 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Mon, 14 Oct 2024 16:44:05 +0000 Subject: [PATCH] feat(netlink): detect ipv6 support level - 'supported' if one ipv6 route is found that is not loopback and not a default route - 'internet' if one default ipv6 route is found --- cmd/gluetun/main.go | 10 +++--- internal/cli/openvpnconfig.go | 12 ++++--- internal/netlink/ipv6.go | 41 +++++++++++++++++------ internal/vpn/loop.go | 63 ++++++++++++++++++----------------- internal/vpn/openvpn.go | 11 +++--- internal/vpn/run.go | 4 +-- internal/vpn/wireguard.go | 8 +++-- 7 files changed, 91 insertions(+), 58 deletions(-) diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index ad8f309e7..57b09c9e8 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -245,10 +245,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return err } - ipv6Supported, err := netLinker.IsIPv6Supported() + ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel() if err != nil { return fmt.Errorf("checking for IPv6 support: %w", err) } + ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported || + ipv6SupportLevel == netlink.IPv6Internet err = allSettings.Validate(storage, ipv6Supported, logger) if err != nil { @@ -429,7 +431,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) vpnLogger := logger.New(log.SetComponent("vpn")) - vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts, + vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts, providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled) @@ -473,7 +475,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, logger.New(log.SetComponent("http server")), allSettings.ControlServer.AuthFilePath, buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper, - storage, ipv6Supported) + storage, ipv6SupportLevel.IsSupported()) if err != nil { return fmt.Errorf("setting up control server: %w", err) } @@ -555,7 +557,7 @@ type netLinker interface { Ruler Linker IsWireguardSupported() (ok bool, err error) - IsIPv6Supported() (ok bool, err error) + FindIPv6SupportLevel() (level netlink.IPv6SupportLevel, err error) PatchLoggerLevel(level log.Level) } diff --git a/internal/cli/openvpnconfig.go b/internal/cli/openvpnconfig.go index 520f824d9..560896611 100644 --- a/internal/cli/openvpnconfig.go +++ b/internal/cli/openvpnconfig.go @@ -11,6 +11,7 @@ import ( "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/storage" @@ -40,7 +41,7 @@ type IPFetcher interface { } type IPv6Checker interface { - IsIPv6Supported() (supported bool, err error) + FindIPv6SupportLevel() (level netlink.IPv6SupportLevel, err error) } func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, @@ -57,12 +58,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, return err } - ipv6Supported, err := ipv6Checker.IsIPv6Supported() + ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel() if err != nil { return fmt.Errorf("checking for IPv6 support: %w", err) } - if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil { + err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger) + if err != nil { return fmt.Errorf("validating settings: %w", err) } @@ -78,13 +80,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) providerConf := providers.Get(allSettings.VPN.Provider.Name) connection, err := providerConf.GetConnection( - allSettings.VPN.Provider.ServerSelection, ipv6Supported) + allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet) if err != nil { return err } lines := providerConf.OpenVPNConfig(connection, - allSettings.VPN.OpenVPN, ipv6Supported) + allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported()) fmt.Println(strings.Join(lines, "\n")) return nil diff --git a/internal/netlink/ipv6.go b/internal/netlink/ipv6.go index d8eff77e6..f87e2ce4b 100644 --- a/internal/netlink/ipv6.go +++ b/internal/netlink/ipv6.go @@ -4,19 +4,37 @@ import ( "fmt" ) -func (n *NetLink) IsIPv6Supported() (supported bool, err error) { +type IPv6SupportLevel uint8 + +const ( + IPv6Unsupported = iota + // IPv6Supported indicates the host supports IPv6 but has no access to the + // Internet via IPv6. It is true if one IPv6 route is found and no default + // IPv6 route is found. + IPv6Supported + // IPv6Internet indicates the host has access to the Internet via IPv6, + // which is detected when a default IPv6 route is found. + IPv6Internet +) + +func (i IPv6SupportLevel) IsSupported() bool { + return i == IPv6Supported || i == IPv6Internet +} + +func (n *NetLink) FindIPv6SupportLevel() (level IPv6SupportLevel, err error) { routes, err := n.RouteList(FamilyV6) if err != nil { - return false, fmt.Errorf("listing IPv6 routes: %w", err) + return IPv6Unsupported, fmt.Errorf("listing IPv6 routes: %w", err) } // Check each route for IPv6 due to Podman bug listing IPv4 routes // as IPv6 routes at container start, see: // https://github.com/qdm12/gluetun/issues/1241#issuecomment-1333405949 + level = IPv6Unsupported for _, route := range routes { link, err := n.LinkByIndex(route.LinkIndex) if err != nil { - return false, fmt.Errorf("finding link corresponding to route: %w", err) + return IPv6Unsupported, fmt.Errorf("finding link corresponding to route: %w", err) } sourceIsIPv6 := route.Src.IsValid() && route.Src.Is6() @@ -24,14 +42,17 @@ func (n *NetLink) IsIPv6Supported() (supported bool, err error) { switch { case !sourceIsIPv6 && !destinationIsIPv6, destinationIsIPv6 && route.Dst.Addr().IsLoopback(): - continue + case route.Dst.Addr().IsUnspecified(): // default ipv6 route + n.debugLogger.Debugf("IPv6 is preferred by link %s", link.Name) + return IPv6Internet, nil + default: // non-default ipv6 route found + n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name) + level = IPv6Supported } - - n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name) - return true, nil } - n.debugLogger.Debugf("IPv6 is not supported after searching %d routes", - len(routes)) - return false, nil + if level == IPv6Unsupported { + n.debugLogger.Debugf("no IPv6 route found in %d routes", len(routes)) + } + return level, nil } diff --git a/internal/vpn/loop.go b/internal/vpn/loop.go index 40bbf4881..8d6164883 100644 --- a/internal/vpn/loop.go +++ b/internal/vpn/loop.go @@ -8,6 +8,7 @@ import ( "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/loopstate" "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/vpn/state" "github.com/qdm12/log" ) @@ -18,10 +19,10 @@ type Loop struct { providers Providers storage Storage // Fixed parameters - buildInfo models.BuildInformation - versionInfo bool - ipv6Supported bool - vpnInputPorts []uint16 // TODO make changeable through stateful firewall + buildInfo models.BuildInformation + versionInfo bool + ipv6SupportLevel netlink.IPv6SupportLevel + vpnInputPorts []uint16 // TODO make changeable through stateful firewall // Configurators openvpnConf OpenVPN netLinker NetLinker @@ -48,8 +49,10 @@ const ( defaultBackoffTime = 15 * time.Second ) -func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16, - providers Providers, storage Storage, openvpnConf OpenVPN, +func NewLoop(vpnSettings settings.VPN, + ipv6SupportLevel netlink.IPv6SupportLevel, + vpnInputPorts []uint16, providers Providers, + storage Storage, openvpnConf OpenVPN, netLinker NetLinker, fw Firewall, routing Routing, portForward PortForward, starter CmdStarter, publicip PublicIPLoop, dnsLooper DNSLoop, @@ -65,29 +68,29 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1 state := state.New(statusManager, vpnSettings) return &Loop{ - statusManager: statusManager, - state: state, - providers: providers, - storage: storage, - buildInfo: buildInfo, - versionInfo: versionInfo, - ipv6Supported: ipv6Supported, - vpnInputPorts: vpnInputPorts, - openvpnConf: openvpnConf, - netLinker: netLinker, - fw: fw, - routing: routing, - portForward: portForward, - publicip: publicip, - dnsLooper: dnsLooper, - starter: starter, - logger: logger, - client: client, - start: start, - running: running, - stop: stop, - stopped: stopped, - userTrigger: true, - backoffTime: defaultBackoffTime, + statusManager: statusManager, + state: state, + providers: providers, + storage: storage, + buildInfo: buildInfo, + versionInfo: versionInfo, + ipv6SupportLevel: ipv6SupportLevel, + vpnInputPorts: vpnInputPorts, + openvpnConf: openvpnConf, + netLinker: netLinker, + fw: fw, + routing: routing, + portForward: portForward, + publicip: publicip, + dnsLooper: dnsLooper, + starter: starter, + logger: logger, + client: client, + start: start, + running: running, + stop: stop, + stopped: stopped, + userTrigger: true, + backoffTime: defaultBackoffTime, } } diff --git a/internal/vpn/openvpn.go b/internal/vpn/openvpn.go index 102640e13..555fb1da7 100644 --- a/internal/vpn/openvpn.go +++ b/internal/vpn/openvpn.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/provider" ) @@ -13,16 +14,18 @@ import ( // It returns a serverName for port forwarding (PIA) and an error if it fails. func setupOpenVPN(ctx context.Context, fw Firewall, openvpnConf OpenVPN, providerConf provider.Provider, - settings settings.VPN, ipv6Supported bool, starter CmdStarter, - logger openvpn.Logger) (runner *openvpn.Runner, serverName string, + settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, + starter CmdStarter, logger openvpn.Logger) ( + runner *openvpn.Runner, serverName string, canPortForward bool, err error, ) { - connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported) + ipv6Internet := ipv6SupportLevel == netlink.IPv6Internet + connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Internet) if err != nil { return nil, "", false, fmt.Errorf("finding a valid server connection: %w", err) } - lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6Supported) + lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6SupportLevel.IsSupported()) if err := openvpnConf.WriteConfig(lines); err != nil { return nil, "", false, fmt.Errorf("writing configuration to file: %w", err) diff --git a/internal/vpn/run.go b/internal/vpn/run.go index a0cc02748..ae6898072 100644 --- a/internal/vpn/run.go +++ b/internal/vpn/run.go @@ -35,11 +35,11 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { if settings.Type == vpn.OpenVPN { vpnInterface = settings.OpenVPN.Interface vpnRunner, serverName, canPortForward, err = setupOpenVPN(ctx, l.fw, - l.openvpnConf, providerConf, settings, l.ipv6Supported, l.starter, subLogger) + l.openvpnConf, providerConf, settings, l.ipv6SupportLevel, l.starter, subLogger) } else { // Wireguard vpnInterface = settings.Wireguard.Interface vpnRunner, serverName, canPortForward, err = setupWireguard(ctx, l.netLinker, l.fw, - providerConf, settings, l.ipv6Supported, subLogger) + providerConf, settings, l.ipv6SupportLevel, subLogger) } if err != nil { l.crashed(ctx, err) diff --git a/internal/vpn/wireguard.go b/internal/vpn/wireguard.go index 7f5c42463..dc3b156e9 100644 --- a/internal/vpn/wireguard.go +++ b/internal/vpn/wireguard.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/wireguard" @@ -15,15 +16,16 @@ import ( // It returns a serverName for port forwarding (PIA) and an error if it fails. func setupWireguard(ctx context.Context, netlinker NetLinker, fw Firewall, providerConf provider.Provider, - settings settings.VPN, ipv6Supported bool, logger wireguard.Logger) ( + settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, logger wireguard.Logger) ( wireguarder *wireguard.Wireguard, serverName string, canPortForward bool, err error, ) { - connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported) + ipv6Internet := ipv6SupportLevel == netlink.IPv6Internet + connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Internet) if err != nil { return nil, "", false, fmt.Errorf("finding a VPN server: %w", err) } - wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6Supported) + wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6SupportLevel.IsSupported()) logger.Debug("Wireguard server public key: " + wireguardSettings.PublicKey) logger.Debug("Wireguard client private key: " + gosettings.ObfuscateKey(wireguardSettings.PrivateKey))