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
This commit is contained in:
Quentin McGaw
2024-10-14 16:44:05 +00:00
parent ffb0bec4da
commit dae44051f6
7 changed files with 92 additions and 61 deletions

View File

@@ -242,10 +242,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
ipv6Supported, err := netLinker.IsIPv6Supported() ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel()
if err != nil { if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err) return fmt.Errorf("checking for IPv6 support: %w", err)
} }
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
ipv6SupportLevel == netlink.IPv6Internet
err = allSettings.Validate(storage, ipv6Supported, logger) err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil { if err != nil {
@@ -430,7 +432,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor) httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn")) 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, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf, providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient, routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled) buildInfo, *allSettings.Version.Enabled)
@@ -474,7 +476,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.New(log.SetComponent("http server")), logger.New(log.SetComponent("http server")),
allSettings.ControlServer.AuthFilePath, allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper, buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported) storage, ipv6SupportLevel.IsSupported())
if err != nil { if err != nil {
return fmt.Errorf("setting up control server: %w", err) return fmt.Errorf("setting up control server: %w", err)
} }
@@ -550,7 +552,7 @@ type netLinker interface {
Ruler Ruler
Linker Linker
IsWireguardSupported() (ok bool, err error) IsWireguardSupported() (ok bool, err error)
IsIPv6Supported() (ok bool, err error) FindIPv6SupportLevel() (level netlink.IPv6SupportLevel, err error)
PatchLoggerLevel(level log.Level) PatchLoggerLevel(level log.Level)
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
@@ -40,7 +41,7 @@ type IPFetcher interface {
} }
type IPv6Checker interface { type IPv6Checker interface {
IsIPv6Supported() (supported bool, err error) FindIPv6SupportLevel() (level netlink.IPv6SupportLevel, err error)
} }
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
@@ -58,12 +59,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
} }
allSettings.SetDefaults() allSettings.SetDefaults()
ipv6Supported, err := ipv6Checker.IsIPv6Supported() ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel()
if err != nil { if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err) 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) return fmt.Errorf("validating settings: %w", err)
} }
@@ -79,13 +81,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
providerConf := providers.Get(allSettings.VPN.Provider.Name) providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection( connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6Supported) allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet)
if err != nil { if err != nil {
return err return err
} }
lines := providerConf.OpenVPNConfig(connection, lines := providerConf.OpenVPNConfig(connection,
allSettings.VPN.OpenVPN, ipv6Supported) allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported())
fmt.Println(strings.Join(lines, "\n")) fmt.Println(strings.Join(lines, "\n"))
return nil return nil

View File

@@ -4,34 +4,56 @@ import (
"fmt" "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) routes, err := n.RouteList(FamilyV6)
if err != nil { 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 // Check each route for IPv6 due to Podman bug listing IPv4 routes
// as IPv6 routes at container start, see: // as IPv6 routes at container start, see:
// https://github.com/qdm12/gluetun/issues/1241#issuecomment-1333405949 // https://github.com/qdm12/gluetun/issues/1241#issuecomment-1333405949
level = IPv6Unsupported
for _, route := range routes { for _, route := range routes {
link, err := n.LinkByIndex(route.LinkIndex) link, err := n.LinkByIndex(route.LinkIndex)
if err != nil { 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() sourceIsIPv4 := route.Src.IsValid() && route.Src.Is4()
destinationIsIPv4 := route.Dst.IsValid() && route.Dst.Addr().Is4()
destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6() destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6()
switch { switch {
case !sourceIsIPv6 && !destinationIsIPv6, case sourceIsIPv4 && destinationIsIPv4,
destinationIsIPv6 && route.Dst.Addr().IsLoopback(): destinationIsIPv6 && route.Dst.Addr().IsLoopback():
continue case route.Dst.Addr().IsUnspecified(): // default ipv6 route
} n.debugLogger.Debugf("IPv6 internet access is enabled on link %s", link.Name)
return IPv6Internet, nil
default: // non-default ipv6 route found
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name) n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
return true, nil level = IPv6Supported
}
} }
n.debugLogger.Debugf("IPv6 is not supported after searching %d routes", if level == IPv6Unsupported {
len(routes)) n.debugLogger.Debugf("no IPv6 route found in %d routes", len(routes))
return false, nil }
return level, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/loopstate" "github.com/qdm12/gluetun/internal/loopstate"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/vpn/state" "github.com/qdm12/gluetun/internal/vpn/state"
"github.com/qdm12/log" "github.com/qdm12/log"
) )
@@ -23,7 +24,7 @@ type Loop struct {
// Fixed parameters // Fixed parameters
buildInfo models.BuildInformation buildInfo models.BuildInformation
versionInfo bool versionInfo bool
ipv6Supported bool ipv6SupportLevel netlink.IPv6SupportLevel
vpnInputPorts []uint16 // TODO make changeable through stateful firewall vpnInputPorts []uint16 // TODO make changeable through stateful firewall
// Configurators // Configurators
openvpnConf OpenVPN openvpnConf OpenVPN
@@ -51,7 +52,7 @@ const (
defaultBackoffTime = 15 * time.Second defaultBackoffTime = 15 * time.Second
) )
func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16, func NewLoop(vpnSettings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, vpnInputPorts []uint16,
providers Providers, storage Storage, healthSettings settings.Health, providers Providers, storage Storage, healthSettings settings.Health,
healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN, healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN,
netLinker NetLinker, fw Firewall, routing Routing, netLinker NetLinker, fw Firewall, routing Routing,
@@ -78,7 +79,7 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
healthServer: healthServer, healthServer: healthServer,
buildInfo: buildInfo, buildInfo: buildInfo,
versionInfo: versionInfo, versionInfo: versionInfo,
ipv6Supported: ipv6Supported, ipv6SupportLevel: ipv6SupportLevel,
vpnInputPorts: vpnInputPorts, vpnInputPorts: vpnInputPorts,
openvpnConf: openvpnConf, openvpnConf: openvpnConf,
netLinker: netLinker, netLinker: netLinker,

View File

@@ -6,6 +6,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
) )
@@ -14,15 +15,16 @@ import (
// It returns a serverName for port forwarding (PIA) and an error if it fails. // It returns a serverName for port forwarding (PIA) and an error if it fails.
func setupOpenVPN(ctx context.Context, fw Firewall, func setupOpenVPN(ctx context.Context, fw Firewall,
openvpnConf OpenVPN, providerConf provider.Provider, openvpnConf OpenVPN, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, starter CmdStarter, settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, starter CmdStarter,
logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error, logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, 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 { if err != nil {
return nil, models.Connection{}, fmt.Errorf("finding a valid server connection: %w", err) return nil, models.Connection{}, 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 { if err := openvpnConf.WriteConfig(lines); err != nil {
return nil, models.Connection{}, fmt.Errorf("writing configuration to file: %w", err) return nil, models.Connection{}, fmt.Errorf("writing configuration to file: %w", err)

View File

@@ -36,11 +36,11 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
if settings.Type == vpn.OpenVPN { if settings.Type == vpn.OpenVPN {
vpnInterface = settings.OpenVPN.Interface vpnInterface = settings.OpenVPN.Interface
vpnRunner, connection, err = setupOpenVPN(ctx, l.fw, vpnRunner, connection, 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 } else { // Wireguard
vpnInterface = settings.Wireguard.Interface vpnInterface = settings.Wireguard.Interface
vpnRunner, connection, err = setupWireguard(ctx, l.netLinker, l.fw, vpnRunner, connection, err = setupWireguard(ctx, l.netLinker, l.fw,
providerConf, settings, l.ipv6Supported, subLogger) providerConf, settings, l.ipv6SupportLevel, subLogger)
} }
if err != nil { if err != nil {
l.crashed(ctx, err) l.crashed(ctx, err)

View File

@@ -6,6 +6,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/utils"
"github.com/qdm12/gluetun/internal/wireguard" "github.com/qdm12/gluetun/internal/wireguard"
@@ -16,15 +17,16 @@ import (
// It returns a serverName for port forwarding (PIA) and an error if it fails. // It returns a serverName for port forwarding (PIA) and an error if it fails.
func setupWireguard(ctx context.Context, netlinker NetLinker, func setupWireguard(ctx context.Context, netlinker NetLinker,
fw Firewall, providerConf provider.Provider, 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, connection models.Connection, err error, wireguarder *wireguard.Wireguard, connection models.Connection, 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 { if err != nil {
return nil, models.Connection{}, fmt.Errorf("finding a VPN server: %w", err) return nil, models.Connection{}, 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 server public key: " + wireguardSettings.PublicKey)
logger.Debug("Wireguard client private key: " + gosettings.ObfuscateKey(wireguardSettings.PrivateKey)) logger.Debug("Wireguard client private key: " + gosettings.ObfuscateKey(wireguardSettings.PrivateKey))