diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go index 9cd3ba752..29238eb06 100644 --- a/net/tsaddr/tsaddr.go +++ b/net/tsaddr/tsaddr.go @@ -309,3 +309,17 @@ func MapVia(siteID uint32, v4 netip.Prefix) (via netip.Prefix, err error) { copy(a[12:], ip4a[:]) return netip.PrefixFrom(netip.AddrFrom16(a), v4.Bits()+64+32), nil } + +// MapViaAddr returns an IPv6 "via" address for an IPv4 address in a given siteID. +func MapViaAddr(siteID uint32, v4 netip.Addr) (netip.Addr, error) { + if !v4.Is4() { + return netip.Addr{}, errors.New("want IPv4 address with a site ID") + } + viaRange16 := TailscaleViaRange().Addr().As16() + var a [16]byte + copy(a[:], viaRange16[:8]) + binary.BigEndian.PutUint32(a[8:], siteID) + ip4a := v4.As4() + copy(a[12:], ip4a[:]) + return netip.AddrFrom16(a), nil +} diff --git a/net/tsaddr/tsaddr_test.go b/net/tsaddr/tsaddr_test.go index 812475ac1..36c3cae3e 100644 --- a/net/tsaddr/tsaddr_test.go +++ b/net/tsaddr/tsaddr_test.go @@ -105,3 +105,36 @@ func TestUnmapVia(t *testing.T) { } } } + +func TestMapViaAddr(t *testing.T) { + tests := []struct { + ip string + site uint32 + want string + }{ + {"1.2.3.4", 0, "fd7a:115c:a1e0:b1a::102:304"}, + {"1.2.3.4", 7, "fd7a:115c:a1e0:b1a:0:7:102:304"}, + } + for _, tt := range tests { + got, err := MapViaAddr(tt.site, netip.MustParseAddr(tt.ip)) + if err != nil { + t.Errorf("for %q @ %d: error: %v", tt.ip, tt.site, err) + continue + } + + if got.String() != tt.want { + t.Errorf("for %q @ %d: got %q, want %q", tt.ip, tt.site, got.String(), tt.want) + } + } + + t.Run("Error", func(t *testing.T) { + addr, err := MapViaAddr(9, netip.MustParseAddr("::1")) + want := "want IPv4 address with a site ID" + if err == nil || err.Error() != want { + t.Errorf("got err=%v; want %q", err, want) + } + if addr.IsValid() { + t.Errorf("expected invalid Addr") + } + }) +}