diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go index b9e341ca8..630ef0bf6 100644 --- a/cmd/containerboot/main.go +++ b/cmd/containerboot/main.go @@ -476,18 +476,20 @@ func main() { newCurentEgressIPs = deephash.Hash(&egressAddrs) egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs if egressIPsHaveChanged && len(egressAddrs) != 0 { + var rulesInstalled bool for _, egressAddr := range egressAddrs { ea := egressAddr.Addr() - // TODO (irbekrm): make it work for IPv6 too. - if ea.Is6() { - log.Println("Not installing egress forwarding rules for IPv6 as this is currently not supported") - continue - } - log.Printf("Installing forwarding rules for destination %v", ea.String()) - if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil { - log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err) + if ea.Is4() || (ea.Is6() && nfr.HasIPV6NAT()) { + rulesInstalled = true + log.Printf("Installing forwarding rules for destination %v", ea.String()) + if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil { + log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err) + } } } + if !rulesInstalled { + log.Fatalf("no forwarding rules for egress addresses %v, host supports IPv6: %v", egressAddrs, nfr.HasIPV6NAT()) + } } currentEgressIPs = newCurentEgressIPs } @@ -941,7 +943,7 @@ func enableIPForwarding(v4Forwarding, v6Forwarding bool, root string) error { return nil } -func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error { +func installEgressForwardingRule(_ context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error { dst, err := netip.ParseAddr(dstStr) if err != nil { return err diff --git a/cmd/containerboot/main_test.go b/cmd/containerboot/main_test.go index e0ddd62bf..5c92787ce 100644 --- a/cmd/containerboot/main_test.go +++ b/cmd/containerboot/main_test.go @@ -52,7 +52,7 @@ func TestContainerBoot(t *testing.T) { } defer kube.Close() - tailscaledConf := &ipn.ConfigVAlpha{AuthKey: func(s string) *string { return &s }("foo"), Version: "alpha0"} + tailscaledConf := &ipn.ConfigVAlpha{AuthKey: ptr.To("foo"), Version: "alpha0"} tailscaledConfBytes, err := json.Marshal(tailscaledConf) if err != nil { t.Fatalf("error unmarshaling tailscaled config: %v", err) @@ -116,6 +116,9 @@ type phase struct { // WantFiles files that should exist in the container and their // contents. WantFiles map[string]string + // WantFatalLog is the fatal log message we expect from containerboot. + // If set for a phase, the test will finish on that phase. + WantFatalLog string } runningNotify := &ipn.Notify{ State: ptr.To(ipn.Running), @@ -349,12 +352,57 @@ type phase struct { "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp", "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key", }, + WantFiles: map[string]string{ + "proc/sys/net/ipv4/ip_forward": "1", + "proc/sys/net/ipv6/conf/all/forwarding": "0", + }, }, { Notify: runningNotify, }, }, }, + { + Name: "egress_proxy_fqdn_ipv6_target_on_ipv4_host", + Env: map[string]string{ + "TS_AUTHKEY": "tskey-key", + "TS_TAILNET_TARGET_FQDN": "ipv6-node.test.ts.net", // resolves to IPv6 address + "TS_USERSPACE": "false", + "TS_TEST_FAKE_NETFILTER_6": "false", + }, + Phases: []phase{ + { + WantCmds: []string{ + "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp", + "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key", + }, + WantFiles: map[string]string{ + "proc/sys/net/ipv4/ip_forward": "1", + "proc/sys/net/ipv6/conf/all/forwarding": "0", + }, + }, + { + Notify: &ipn.Notify{ + State: ptr.To(ipn.Running), + NetMap: &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + StableID: tailcfg.StableNodeID("myID"), + Name: "test-node.test.ts.net", + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, + }).View(), + Peers: []tailcfg.NodeView{ + (&tailcfg.Node{ + StableID: tailcfg.StableNodeID("ipv6ID"), + Name: "ipv6-node.test.ts.net", + Addresses: []netip.Prefix{netip.MustParsePrefix("::1/128")}, + }).View(), + }, + }, + }, + WantFatalLog: "no forwarding rules for egress addresses [::1/128], host supports IPv6: false", + }, + }, + }, { Name: "authkey_once", Env: map[string]string{ @@ -697,6 +745,25 @@ type phase struct { var wantCmds []string for i, p := range test.Phases { lapi.Notify(p.Notify) + if p.WantFatalLog != "" { + err := tstest.WaitFor(2*time.Second, func() error { + state, err := cmd.Process.Wait() + if err != nil { + return err + } + if state.ExitCode() != 1 { + return fmt.Errorf("process exited with code %d but wanted %d", state.ExitCode(), 1) + } + waitLogLine(t, time.Second, cbOut, p.WantFatalLog) + return nil + }) + if err != nil { + t.Fatal(err) + } + + // Early test return, we don't expect the successful startup log message. + return + } wantCmds = append(wantCmds, p.WantCmds...) waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n")) err := tstest.WaitFor(2*time.Second, func() error { diff --git a/cmd/k8s-operator/sts.go b/cmd/k8s-operator/sts.go index b52fdcf69..17cc047d0 100644 --- a/cmd/k8s-operator/sts.go +++ b/cmd/k8s-operator/sts.go @@ -294,6 +294,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l Selector: map[string]string{ "app": sts.ParentResourceUID, }, + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack), }, } logger.Debugf("reconciling headless service for StatefulSet") diff --git a/cmd/k8s-operator/testutils_test.go b/cmd/k8s-operator/testutils_test.go index 9c0f78b09..f5f9ece2b 100644 --- a/cmd/k8s-operator/testutils_test.go +++ b/cmd/k8s-operator/testutils_test.go @@ -319,7 +319,8 @@ func expectedHeadlessService(name string, parentType string) *corev1.Service { Selector: map[string]string{ "app": "1234-UID", }, - ClusterIP: "None", + ClusterIP: "None", + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack), }, } } diff --git a/util/linuxfw/fake.go b/util/linuxfw/fake.go index c3c9ed00b..63a728d55 100644 --- a/util/linuxfw/fake.go +++ b/util/linuxfw/fake.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" + "os" + "strconv" "strings" ) @@ -128,8 +130,13 @@ func (n *fakeIPTables) DeleteChain(table, chain string) error { func NewFakeIPTablesRunner() *iptablesRunner { ipt4 := newFakeIPTables() - ipt6 := newFakeIPTables() + v6Available := false + var ipt6 iptablesInterface + if use6, err := strconv.ParseBool(os.Getenv("TS_TEST_FAKE_NETFILTER_6")); use6 || err != nil { + ipt6 = newFakeIPTables() + v6Available = true + } - iptr := &iptablesRunner{ipt4, ipt6, true, true, true} + iptr := &iptablesRunner{ipt4, ipt6, v6Available, v6Available, v6Available} return iptr }