mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
cmd/containerboot,cmd/k8s-operator: enable IPv6 for fqdn egress proxies (#12577)
cmd/containerboot,cmd/k8s-operator: enable IPv6 for fqdn egress proxies Don't skip installing egress forwarding rules for IPv6 (as long as the host supports IPv6), and set headless services `ipFamilyPolicy` to `PreferDualStack` to optionally enable both IP families when possible. Note that even with `PreferDualStack` set, testing a dual-stack GKE cluster with the default DNS setup of kube-dns did not correctly set both A and AAAA records for the headless service, and instead only did so when switching the cluster DNS to Cloud DNS. For both IPv4 and IPv6 to work simultaneously in a dual-stack cluster, we require headless services to return both A and AAAA records. If the host doesn't support IPv6 but the FQDN specified only has IPv6 addresses available, containerboot will exit with error code 1 and an error message because there is no viable egress route. Fixes #12215 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
parent
309afa53cf
commit
01a7726cf7
@ -476,18 +476,20 @@ runLoop:
|
|||||||
newCurentEgressIPs = deephash.Hash(&egressAddrs)
|
newCurentEgressIPs = deephash.Hash(&egressAddrs)
|
||||||
egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs
|
egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs
|
||||||
if egressIPsHaveChanged && len(egressAddrs) != 0 {
|
if egressIPsHaveChanged && len(egressAddrs) != 0 {
|
||||||
|
var rulesInstalled bool
|
||||||
for _, egressAddr := range egressAddrs {
|
for _, egressAddr := range egressAddrs {
|
||||||
ea := egressAddr.Addr()
|
ea := egressAddr.Addr()
|
||||||
// TODO (irbekrm): make it work for IPv6 too.
|
if ea.Is4() || (ea.Is6() && nfr.HasIPV6NAT()) {
|
||||||
if ea.Is6() {
|
rulesInstalled = true
|
||||||
log.Println("Not installing egress forwarding rules for IPv6 as this is currently not supported")
|
log.Printf("Installing forwarding rules for destination %v", ea.String())
|
||||||
continue
|
if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil {
|
||||||
}
|
log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err)
|
||||||
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
|
currentEgressIPs = newCurentEgressIPs
|
||||||
}
|
}
|
||||||
@ -941,7 +943,7 @@ func enableIPForwarding(v4Forwarding, v6Forwarding bool, root string) error {
|
|||||||
return nil
|
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)
|
dst, err := netip.ParseAddr(dstStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -52,7 +52,7 @@ func TestContainerBoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer kube.Close()
|
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)
|
tailscaledConfBytes, err := json.Marshal(tailscaledConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error unmarshaling tailscaled config: %v", err)
|
t.Fatalf("error unmarshaling tailscaled config: %v", err)
|
||||||
@ -116,6 +116,9 @@ func TestContainerBoot(t *testing.T) {
|
|||||||
// WantFiles files that should exist in the container and their
|
// WantFiles files that should exist in the container and their
|
||||||
// contents.
|
// contents.
|
||||||
WantFiles map[string]string
|
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{
|
runningNotify := &ipn.Notify{
|
||||||
State: ptr.To(ipn.Running),
|
State: ptr.To(ipn.Running),
|
||||||
@ -349,12 +352,57 @@ func TestContainerBoot(t *testing.T) {
|
|||||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
"/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",
|
"/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,
|
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",
|
Name: "authkey_once",
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
@ -697,6 +745,25 @@ func TestContainerBoot(t *testing.T) {
|
|||||||
var wantCmds []string
|
var wantCmds []string
|
||||||
for i, p := range test.Phases {
|
for i, p := range test.Phases {
|
||||||
lapi.Notify(p.Notify)
|
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...)
|
wantCmds = append(wantCmds, p.WantCmds...)
|
||||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
||||||
err := tstest.WaitFor(2*time.Second, func() error {
|
err := tstest.WaitFor(2*time.Second, func() error {
|
||||||
|
@ -294,6 +294,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l
|
|||||||
Selector: map[string]string{
|
Selector: map[string]string{
|
||||||
"app": sts.ParentResourceUID,
|
"app": sts.ParentResourceUID,
|
||||||
},
|
},
|
||||||
|
IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
logger.Debugf("reconciling headless service for StatefulSet")
|
logger.Debugf("reconciling headless service for StatefulSet")
|
||||||
|
@ -319,7 +319,8 @@ func expectedHeadlessService(name string, parentType string) *corev1.Service {
|
|||||||
Selector: map[string]string{
|
Selector: map[string]string{
|
||||||
"app": "1234-UID",
|
"app": "1234-UID",
|
||||||
},
|
},
|
||||||
ClusterIP: "None",
|
ClusterIP: "None",
|
||||||
|
IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ package linuxfw
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -128,8 +130,13 @@ func (n *fakeIPTables) DeleteChain(table, chain string) error {
|
|||||||
|
|
||||||
func NewFakeIPTablesRunner() *iptablesRunner {
|
func NewFakeIPTablesRunner() *iptablesRunner {
|
||||||
ipt4 := newFakeIPTables()
|
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
|
return iptr
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user