From c1cb3efbba1f4bf854110e26df900b102357bd6a Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 18 Jul 2022 16:56:10 -0700 Subject: [PATCH] net/netcheck: test for OS IPv6 support as well as connectivity. This lets us distinguish "no IPv6 because the device's ISP doesn't offer IPv6" from "IPv6 is unavailable/disabled in the OS". Signed-off-by: David Anderson --- cmd/tailscale/cli/netcheck.go | 4 +++- net/netcheck/netcheck.go | 9 +++++++++ net/netcheck/netcheck_test.go | 3 +++ tailcfg/tailcfg.go | 9 +++++++-- tailcfg/tailcfg_clone.go | 1 + tailcfg/tailcfg_test.go | 1 + tailcfg/tailcfg_view.go | 2 ++ 7 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cmd/tailscale/cli/netcheck.go b/cmd/tailscale/cli/netcheck.go index 41f353f98..a835b9189 100644 --- a/cmd/tailscale/cli/netcheck.go +++ b/cmd/tailscale/cli/netcheck.go @@ -126,8 +126,10 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error { printf("\t* IPv6: yes, %v\n", report.GlobalV6) } else if report.IPv6 { printf("\t* IPv6: (no addr found)\n") + } else if report.OSHasIPv6 { + printf("\t* IPv6: no, but OS has support\n") } else { - printf("\t* IPv6: no\n") + printf("\t* IPv6: no, unavailable in OS\n") } printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP) printf("\t* HairPinning: %v\n", report.HairPinning) diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index f8388eabe..535cb39dd 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -76,6 +76,7 @@ type Report struct { IPv4 bool // an IPv4 STUN round trip completed IPv6CanSend bool // an IPv6 packet was able to be sent IPv4CanSend bool // an IPv4 packet was able to be sent + OSHasIPv6 bool // could bind a socket to ::1 // MappingVariesByDestIP is whether STUN results depend which // STUN server you're talking to (on IPv4). @@ -806,6 +807,14 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, return nil, err } + // See if IPv6 works at all, or if it's been hard disabled at the + // OS level. + v6udp, err := netns.Listener(c.logf).ListenPacket(ctx, "udp6", "[::1]:0") + if err == nil { + rs.report.OSHasIPv6 = true + v6udp.Close() + } + // Create a UDP4 socket used for sending to our discovered IPv4 address. rs.pc4Hair, err = netns.Listener(c.logf).ListenPacket(ctx, "udp4", ":0") if err != nil { diff --git a/net/netcheck/netcheck_test.go b/net/netcheck/netcheck_test.go index e14194073..a6c843af7 100644 --- a/net/netcheck/netcheck_test.go +++ b/net/netcheck/netcheck_test.go @@ -111,6 +111,9 @@ func TestWorksWhenUDPBlocked(t *testing.T) { // That's not relevant to this test, so just accept what we're // given. want.IPv4CanSend = r.IPv4CanSend + // OS IPv6 test is irrelevant here, accept whatever the current + // machine has. + want.OSHasIPv6 = r.OSHasIPv6 if !reflect.DeepEqual(r, want) { t.Errorf("mismatch\n got: %+v\nwant: %+v\n", r, want) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index dba4f6ccc..2ac946ff0 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -496,10 +496,14 @@ type NetInfo struct { // It reports true even if there's no NAT involved. HairPinning opt.Bool - // WorkingIPv6 is whether IPv6 works. + // WorkingIPv6 is whether the host has IPv6 internet connectivity. WorkingIPv6 opt.Bool - // WorkingUDP is whether UDP works. + // OSHasIPv6 is whether the OS supports IPv6 at all, regardless of + // whether IPv6 internet connectivity is available. + OSHasIPv6 opt.Bool + + // WorkingUDP is whether the host has UDP internet connectivity. WorkingUDP opt.Bool // HavePortMap is whether we have an existing portmap open @@ -590,6 +594,7 @@ func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool { return ni.MappingVariesByDestIP == ni2.MappingVariesByDestIP && ni.HairPinning == ni2.HairPinning && ni.WorkingIPv6 == ni2.WorkingIPv6 && + ni.OSHasIPv6 == ni2.OSHasIPv6 && ni.WorkingUDP == ni2.WorkingUDP && ni.HavePortMap == ni2.HavePortMap && ni.UPnP == ni2.UPnP && diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 41ececac3..9952fe773 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -154,6 +154,7 @@ var _NetInfoCloneNeedsRegeneration = NetInfo(struct { MappingVariesByDestIP opt.Bool HairPinning opt.Bool WorkingIPv6 opt.Bool + OSHasIPv6 opt.Bool WorkingUDP opt.Bool HavePortMap bool UPnP opt.Bool diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index fe09defdd..4e20bc670 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -500,6 +500,7 @@ func TestNetInfoFields(t *testing.T) { "MappingVariesByDestIP", "HairPinning", "WorkingIPv6", + "OSHasIPv6", "WorkingUDP", "HavePortMap", "UPnP", diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index ac65f7f1a..1834a4992 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -338,6 +338,7 @@ func (v *NetInfoView) UnmarshalJSON(b []byte) error { func (v NetInfoView) MappingVariesByDestIP() opt.Bool { return v.ж.MappingVariesByDestIP } func (v NetInfoView) HairPinning() opt.Bool { return v.ж.HairPinning } func (v NetInfoView) WorkingIPv6() opt.Bool { return v.ж.WorkingIPv6 } +func (v NetInfoView) OSHasIPv6() opt.Bool { return v.ж.OSHasIPv6 } func (v NetInfoView) WorkingUDP() opt.Bool { return v.ж.WorkingUDP } func (v NetInfoView) HavePortMap() bool { return v.ж.HavePortMap } func (v NetInfoView) UPnP() opt.Bool { return v.ж.UPnP } @@ -354,6 +355,7 @@ var _NetInfoViewNeedsRegeneration = NetInfo(struct { MappingVariesByDestIP opt.Bool HairPinning opt.Bool WorkingIPv6 opt.Bool + OSHasIPv6 opt.Bool WorkingUDP opt.Bool HavePortMap bool UPnP opt.Bool