diff --git a/Dockerfile b/Dockerfile index 1415abcbe..6666d1af6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -159,6 +159,8 @@ ENV VPN_SERVICE_PROVIDER=pia \ FIREWALL_INPUT_PORTS= \ FIREWALL_OUTBOUND_SUBNETS= \ FIREWALL_DEBUG=off \ + # IPv6 + IPV6_CHECK_ADDRESS=[2606:4700::6810:84e5]:443 \ # Logging LOG_LEVEL=info \ # Health diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index c5b471264..962ba2199 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io/fs" "net/http" + "net/netip" "os" "os/exec" "os/signal" @@ -242,10 +243,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return err } - ipv6Supported, err := netLinker.IsIPv6Supported() + ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx, + allSettings.IPv6.CheckAddress, firewallConf) 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 { @@ -423,7 +427,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), 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) @@ -467,7 +471,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) } @@ -549,7 +553,9 @@ type netLinker interface { Ruler Linker IsWireguardSupported() (ok bool, err error) - IsIPv6Supported() (ok bool, err error) + FindIPv6SupportLevel(ctx context.Context, + checkAddress netip.AddrPort, firewall netlink.Firewall, + ) (level netlink.IPv6SupportLevel, err error) PatchLoggerLevel(level log.Level) } diff --git a/internal/cli/noopfirewall.go b/internal/cli/noopfirewall.go new file mode 100644 index 000000000..7483133e6 --- /dev/null +++ b/internal/cli/noopfirewall.go @@ -0,0 +1,14 @@ +package cli + +import ( + "context" + "net/netip" +) + +type noopFirewall struct{} + +func (f *noopFirewall) AcceptOutput(_ context.Context, _, _ string, _ netip.Addr, + _ uint16, _ bool, +) (err error) { + return nil +} diff --git a/internal/cli/openvpnconfig.go b/internal/cli/openvpnconfig.go index 520f824d9..824f4403b 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,9 @@ type IPFetcher interface { } type IPv6Checker interface { - IsIPv6Supported() (supported bool, err error) + FindIPv6SupportLevel(ctx context.Context, + checkAddress netip.AddrPort, firewall netlink.Firewall, + ) (level netlink.IPv6SupportLevel, err error) } func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, @@ -57,12 +60,14 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, return err } - ipv6Supported, err := ipv6Checker.IsIPv6Supported() + ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(), + allSettings.IPv6.CheckAddress, &noopFirewall{}) 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 +83,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/configuration/settings/ipv6.go b/internal/configuration/settings/ipv6.go new file mode 100644 index 000000000..b10cfa7ec --- /dev/null +++ b/internal/configuration/settings/ipv6.go @@ -0,0 +1,51 @@ +package settings + +import ( + "net/netip" + + "github.com/qdm12/gosettings" + "github.com/qdm12/gosettings/reader" + "github.com/qdm12/gotree" +) + +// IPv6 contains settings regarding IPv6 configuration. +type IPv6 struct { + // CheckAddress is the TCP ip:port address to dial to check + // IPv6 is supported, in case a default IPv6 route is found. + // It defaults to cloudflare.com address [2606:4700::6810:84e5]:443 + CheckAddress netip.AddrPort +} + +func (i IPv6) validate() (err error) { + return nil +} + +func (i *IPv6) copy() (copied IPv6) { + return IPv6{ + CheckAddress: i.CheckAddress, + } +} + +func (i *IPv6) overrideWith(other IPv6) { + i.CheckAddress = gosettings.OverrideWithValidator(i.CheckAddress, other.CheckAddress) +} + +func (i *IPv6) setDefaults() { + defaultCheckAddress := netip.MustParseAddrPort("[2606:4700::6810:84e5]:443") + i.CheckAddress = gosettings.DefaultComparable(i.CheckAddress, defaultCheckAddress) +} + +func (i IPv6) String() string { + return i.toLinesNode().String() +} + +func (i IPv6) toLinesNode() (node *gotree.Node) { + node = gotree.New("IPv6 settings:") + node.Appendf("Check address: %s", i.CheckAddress) + return node +} + +func (i *IPv6) read(r *reader.Reader) (err error) { + i.CheckAddress, err = r.NetipAddrPort("IPV6_CHECK_ADDRESS") + return err +} diff --git a/internal/configuration/settings/settings.go b/internal/configuration/settings/settings.go index eb34d7d13..e2ede3204 100644 --- a/internal/configuration/settings/settings.go +++ b/internal/configuration/settings/settings.go @@ -27,6 +27,7 @@ type Settings struct { Updater Updater Version Version VPN VPN + IPv6 IPv6 Pprof pprof.Settings } @@ -53,6 +54,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support "system": s.System.validate, "updater": s.Updater.Validate, "version": s.Version.validate, + "ipv6": s.IPv6.validate, // Pprof validation done in pprof constructor "VPN": func() error { return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner) @@ -85,6 +87,7 @@ func (s *Settings) copy() (copied Settings) { Version: s.Version.copy(), VPN: s.VPN.Copy(), Pprof: s.Pprof.Copy(), + IPv6: s.IPv6.copy(), } } @@ -106,6 +109,7 @@ func (s *Settings) OverrideWith(other Settings, patchedSettings.Version.overrideWith(other.Version) patchedSettings.VPN.OverrideWith(other.VPN) patchedSettings.Pprof.OverrideWith(other.Pprof) + patchedSettings.IPv6.overrideWith(other.IPv6) err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner) if err != nil { return err @@ -121,6 +125,7 @@ func (s *Settings) SetDefaults() { s.Health.SetDefaults() s.HTTPProxy.setDefaults() s.Log.setDefaults() + s.IPv6.setDefaults() s.PublicIP.setDefaults() s.Shadowsocks.setDefaults() s.Storage.setDefaults() @@ -142,6 +147,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) { node.AppendNode(s.DNS.toLinesNode()) node.AppendNode(s.Firewall.toLinesNode()) node.AppendNode(s.Log.toLinesNode()) + node.AppendNode(s.IPv6.toLinesNode()) node.AppendNode(s.Health.toLinesNode()) node.AppendNode(s.Shadowsocks.toLinesNode()) node.AppendNode(s.HTTPProxy.toLinesNode()) @@ -208,6 +214,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) { "updater": s.Updater.read, "version": s.Version.read, "VPN": s.VPN.read, + "IPv6": s.IPv6.read, "profiling": s.Pprof.Read, } diff --git a/internal/configuration/settings/settings_test.go b/internal/configuration/settings/settings_test.go index 7aa30a15f..ea2a928f1 100644 --- a/internal/configuration/settings/settings_test.go +++ b/internal/configuration/settings/settings_test.go @@ -55,6 +55,8 @@ func Test_Settings_String(t *testing.T) { | └── Enabled: yes ├── Log settings: | └── Log level: INFO +├── IPv6 settings: +| └── Check address: [2606:4700::6810:84e5]:443 ├── Health settings: | ├── Server listening address: 127.0.0.1:9999 | ├── Target address: cloudflare.com:443 diff --git a/internal/firewall/iptables.go b/internal/firewall/iptables.go index 0b1002f55..b9b9cbd47 100644 --- a/internal/firewall/iptables.go +++ b/internal/firewall/iptables.go @@ -162,6 +162,24 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context, return c.runIP6tablesInstruction(ctx, instruction) } +func (c *Config) AcceptOutput(ctx context.Context, + protocol, intf string, ip netip.Addr, port uint16, remove bool, +) error { + interfaceFlag := "-o " + intf + if intf == "*" { // all interfaces + interfaceFlag = "" + } + + instruction := fmt.Sprintf("%s OUTPUT -d %s %s -p %s -m %s --dport %d -j ACCEPT", + appendOrDelete(remove), ip, interfaceFlag, protocol, protocol, port) + if ip.Is4() { + return c.runIptablesInstruction(ctx, instruction) + } else if c.ip6Tables == "" { + return fmt.Errorf("accept output to VPN server: %w", ErrNeedIP6Tables) + } + return c.runIP6tablesInstruction(ctx, instruction) +} + // Thanks to @npawelek. func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context, intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool, diff --git a/internal/netlink/interfaces.go b/internal/netlink/interfaces.go index 7e9e9ac1b..0ff49beb7 100644 --- a/internal/netlink/interfaces.go +++ b/internal/netlink/interfaces.go @@ -1,9 +1,19 @@ package netlink -import "github.com/qdm12/log" +import ( + "context" + "net/netip" + + "github.com/qdm12/log" +) type DebugLogger interface { Debug(message string) Debugf(format string, args ...any) Patch(options ...log.Option) } + +type Firewall interface { + AcceptOutput(ctx context.Context, protocol, intf string, ip netip.Addr, + port uint16, remove bool) (err error) +} diff --git a/internal/netlink/ipv6.go b/internal/netlink/ipv6.go index d8eff77e6..ff7e7b48d 100644 --- a/internal/netlink/ipv6.go +++ b/internal/netlink/ipv6.go @@ -1,37 +1,106 @@ package netlink import ( + "context" "fmt" + "net" + "net/netip" + "time" ) -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(ctx context.Context, + checkAddress netip.AddrPort, firewall Firewall, +) (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() + sourceIsIPv4 := route.Src.IsValid() && route.Src.Is4() + destinationIsIPv4 := route.Dst.IsValid() && route.Dst.Addr().Is4() destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6() switch { - case !sourceIsIPv6 && !destinationIsIPv6, + case sourceIsIPv4 && destinationIsIPv4, destinationIsIPv6 && route.Dst.Addr().IsLoopback(): - continue + case route.Dst.Addr().IsUnspecified(): // default ipv6 route + n.debugLogger.Debugf("IPv6 default route found on link %s", link.Name) + err = dialAddrThroughFirewall(ctx, link.Name, checkAddress, firewall) + if err != nil { + n.debugLogger.Debugf("IPv6 query failed on %s: %w", link.Name, err) + level = IPv6Supported + continue + } + n.debugLogger.Debugf("IPv6 internet is accessible through 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 } + } + + if level == IPv6Unsupported { + n.debugLogger.Debugf("no IPv6 route found in %d routes", len(routes)) + } + return level, nil +} - n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name) - return true, nil +func dialAddrThroughFirewall(ctx context.Context, intf string, + checkAddress netip.AddrPort, firewall Firewall, +) (err error) { + const protocol = "tcp" + remove := false + err = firewall.AcceptOutput(ctx, protocol, intf, + checkAddress.Addr(), checkAddress.Port(), remove) + if err != nil { + return fmt.Errorf("accepting output traffic: %w", err) + } + defer func() { + remove = true + firewallErr := firewall.AcceptOutput(ctx, protocol, intf, + checkAddress.Addr(), checkAddress.Port(), remove) + if err == nil && firewallErr != nil { + err = fmt.Errorf("removing output traffic rule: %w", firewallErr) + } + }() + + dialer := &net.Dialer{ + Timeout: time.Second, + } + conn, err := dialer.DialContext(ctx, protocol, checkAddress.String()) + if err != nil { + return fmt.Errorf("dialing: %w", err) + } + err = conn.Close() + if err != nil { + return fmt.Errorf("closing connection: %w", err) } - n.debugLogger.Debugf("IPv6 is not supported after searching %d routes", - len(routes)) - return false, nil + return nil } diff --git a/internal/netlink/ipv6_test.go b/internal/netlink/ipv6_test.go new file mode 100644 index 000000000..cbfac04e1 --- /dev/null +++ b/internal/netlink/ipv6_test.go @@ -0,0 +1,166 @@ +package netlink + +import ( + "context" + "errors" + "net" + "net/netip" + "strings" + "testing" + "time" + + gomock "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func isIPv6LocallySupported() bool { + dialer := net.Dialer{Timeout: time.Millisecond} + _, err := dialer.Dial("tcp6", "[::1]:9999") + return !strings.HasSuffix(err.Error(), "connect: cannot assign requested address") +} + +// Susceptible to TOCTOU but it should be fine for the use case. +func findAvailableTCPPort(t *testing.T) (port uint16) { + t.Helper() + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + addr := listener.Addr().String() + err = listener.Close() + require.NoError(t, err) + + addrPort, err := netip.ParseAddrPort(addr) + require.NoError(t, err) + + return addrPort.Port() +} + +func Test_dialAddrThroughFirewall(t *testing.T) { + t.Parallel() + + errTest := errors.New("test error") + + const ipv6InternetWorks = false + + testCases := map[string]struct { + getIPv6CheckAddr func(t *testing.T) netip.AddrPort + firewallAddErr error + firewallRemoveErr error + errMessageRegex func() string + }{ + "cloudflare.com": { + getIPv6CheckAddr: func(_ *testing.T) netip.AddrPort { + return netip.MustParseAddrPort("[2606:4700::6810:84e5]:443") + }, + errMessageRegex: func() string { + if ipv6InternetWorks { + return "" + } + return "dialing: dial tcp \\[2606:4700::6810:84e5\\]:443: " + + "connect: (cannot assign requested address|network is unreachable)" + }, + }, + "local_server": { + getIPv6CheckAddr: func(t *testing.T) netip.AddrPort { + t.Helper() + + network := "tcp6" + loopback := netip.MustParseAddr("::1") + if !isIPv6LocallySupported() { + network = "tcp4" + loopback = netip.MustParseAddr("127.0.0.1") + } + + listener, err := net.ListenTCP(network, nil) + require.NoError(t, err) + t.Cleanup(func() { + err := listener.Close() + require.NoError(t, err) + }) + addrPort := netip.MustParseAddrPort(listener.Addr().String()) + return netip.AddrPortFrom(loopback, addrPort.Port()) + }, + }, + "no_local_server": { + getIPv6CheckAddr: func(t *testing.T) netip.AddrPort { + t.Helper() + + loopback := netip.MustParseAddr("::1") + if !ipv6InternetWorks { + loopback = netip.MustParseAddr("127.0.0.1") + } + + availablePort := findAvailableTCPPort(t) + return netip.AddrPortFrom(loopback, availablePort) + }, + errMessageRegex: func() string { + return "dialing: dial tcp (\\[::1\\]|127\\.0\\.0\\.1):[1-9][0-9]{1,4}: " + + "connect: connection refused" + }, + }, + "firewall_add_error": { + firewallAddErr: errTest, + errMessageRegex: func() string { + return "accepting output traffic: test error" + }, + }, + "firewall_remove_error": { + getIPv6CheckAddr: func(t *testing.T) netip.AddrPort { + t.Helper() + + network := "tcp4" + loopback := netip.MustParseAddr("127.0.0.1") + listener, err := net.ListenTCP(network, nil) + require.NoError(t, err) + t.Cleanup(func() { + err := listener.Close() + require.NoError(t, err) + }) + addrPort := netip.MustParseAddrPort(listener.Addr().String()) + return netip.AddrPortFrom(loopback, addrPort.Port()) + }, + firewallRemoveErr: errTest, + errMessageRegex: func() string { + return "removing output traffic rule: test error" + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + var checkAddr netip.AddrPort + if testCase.getIPv6CheckAddr != nil { + checkAddr = testCase.getIPv6CheckAddr(t) + } + + ctx := context.Background() + const intf = "eth0" + firewall := NewMockFirewall(ctrl) + call := firewall.EXPECT().AcceptOutput(ctx, "tcp", intf, + checkAddr.Addr(), checkAddr.Port(), false). + Return(testCase.firewallAddErr) + if testCase.firewallAddErr == nil { + firewall.EXPECT().AcceptOutput(ctx, "tcp", intf, + checkAddr.Addr(), checkAddr.Port(), true). + Return(testCase.firewallRemoveErr).After(call) + } + + err := dialAddrThroughFirewall(ctx, intf, checkAddr, firewall) + var errMessageRegex string + if testCase.errMessageRegex != nil { + errMessageRegex = testCase.errMessageRegex() + } + if errMessageRegex == "" { + assert.NoError(t, err) + } else { + require.Error(t, err) + assert.Regexp(t, errMessageRegex, err.Error()) + } + }) + } +} diff --git a/internal/netlink/mocks_generate_test.go b/internal/netlink/mocks_generate_test.go new file mode 100644 index 000000000..b34cbeef2 --- /dev/null +++ b/internal/netlink/mocks_generate_test.go @@ -0,0 +1,3 @@ +package netlink + +//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Firewall diff --git a/internal/netlink/mocks_test.go b/internal/netlink/mocks_test.go new file mode 100644 index 000000000..353816d8e --- /dev/null +++ b/internal/netlink/mocks_test.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/qdm12/gluetun/internal/netlink (interfaces: Firewall) + +// Package netlink is a generated GoMock package. +package netlink + +import ( + context "context" + netip "net/netip" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockFirewall is a mock of Firewall interface. +type MockFirewall struct { + ctrl *gomock.Controller + recorder *MockFirewallMockRecorder +} + +// MockFirewallMockRecorder is the mock recorder for MockFirewall. +type MockFirewallMockRecorder struct { + mock *MockFirewall +} + +// NewMockFirewall creates a new mock instance. +func NewMockFirewall(ctrl *gomock.Controller) *MockFirewall { + mock := &MockFirewall{ctrl: ctrl} + mock.recorder = &MockFirewallMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFirewall) EXPECT() *MockFirewallMockRecorder { + return m.recorder +} + +// AcceptOutput mocks base method. +func (m *MockFirewall) AcceptOutput(arg0 context.Context, arg1, arg2 string, arg3 netip.Addr, arg4 uint16, arg5 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcceptOutput", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(error) + return ret0 +} + +// AcceptOutput indicates an expected call of AcceptOutput. +func (mr *MockFirewallMockRecorder) AcceptOutput(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptOutput", reflect.TypeOf((*MockFirewall)(nil).AcceptOutput), arg0, arg1, arg2, arg3, arg4, arg5) +} 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))