diff --git a/ipn/conf.go b/ipn/conf.go index de127a28a..342ab684e 100644 --- a/ipn/conf.go +++ b/ipn/conf.go @@ -52,6 +52,15 @@ type ConfigVAlpha struct { // should advertise amongst its wireguard endpoints. StaticEndpoints []netip.AddrPort `json:",omitempty"` + // RelayServerPort is the UDP port for the relay server to bind to. + // A value of 0 will pick a random unused port. Nil disables relay server. + RelayServerPort *uint16 `json:",omitzero"` + + // RelayServerStaticEndpoints are static IP:port endpoints to advertise + // as candidates for relay connections. Only relevant when RelayServerPort + // is non-nil. + RelayServerStaticEndpoints []netip.AddrPort `json:",omitempty"` + // TODO(bradfitz,maisem): future something like: // Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID) } @@ -166,5 +175,13 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) { if c.AdvertiseServices != nil { mp.AdvertiseServices = c.AdvertiseServices } + mp.RelayServerPortSet = true + mp.RelayServerStaticEndpointsSet = true + if c.RelayServerPort != nil { + mp.RelayServerPort = c.RelayServerPort + } + if c.RelayServerStaticEndpoints != nil { + mp.RelayServerStaticEndpoints = c.RelayServerStaticEndpoints + } return mp, nil } diff --git a/ipn/conf_test.go b/ipn/conf_test.go index 41b5c4506..c2f6f9fcf 100644 --- a/ipn/conf_test.go +++ b/ipn/conf_test.go @@ -5,6 +5,8 @@ package ipn import ( "net/netip" + "reflect" + "slices" "testing" ) @@ -64,3 +66,123 @@ func TestConfigVAlpha_ToPrefs_AdvertiseRoutes(t *testing.T) { }) } } + +func TestConfigVAlphaToPrefs(t *testing.T) { + aps := func(t *testing.T, strs ...string) (ret []netip.AddrPort) { + t.Helper() + for _, s := range strs { + n, err := netip.ParseAddrPort(s) + if err != nil { + t.Fatalf("parse AddrPort %q: %v", s, err) + } + ret = append(ret, n) + } + return ret + } + + tests := []struct { + name string + cfg *ConfigVAlpha + want MaskedPrefs + }{ + { + name: "nil_config", + cfg: nil, + want: MaskedPrefs{}, + }, + { + name: "relay_server_port_and_static_endpoints", + cfg: &ConfigVAlpha{ + RelayServerPort: new(uint16(12345)), + RelayServerStaticEndpoints: aps(t, "[2001:db8::1]:40000", "192.0.2.1:40000"), + }, + want: MaskedPrefs{ + Prefs: Prefs{ + RelayServerPort: new(uint16(12345)), + RelayServerStaticEndpoints: aps(t, "[2001:db8::1]:40000", "192.0.2.1:40000"), + }, + RelayServerPortSet: true, + RelayServerStaticEndpointsSet: true, + }, + }, + { + // Port 0 means "pick a random unused port"; it is + // distinct from a nil port which disables the relay + // server. The zero value must be propagated. + name: "relay_server_port_zero_is_random_not_disabled", + cfg: &ConfigVAlpha{ + RelayServerPort: new(uint16(0)), + }, + want: MaskedPrefs{ + Prefs: Prefs{ + RelayServerPort: new(uint16(0)), + }, + RelayServerPortSet: true, + RelayServerStaticEndpointsSet: true, + }, + }, + { + name: "only_static_endpoints_set", + cfg: &ConfigVAlpha{ + RelayServerStaticEndpoints: aps(t, "192.0.2.1:40000"), + }, + want: MaskedPrefs{ + Prefs: Prefs{ + RelayServerStaticEndpoints: aps(t, "192.0.2.1:40000"), + }, + RelayServerPortSet: true, + RelayServerStaticEndpointsSet: true, + }, + }, + { + // Both fields nil but masks set: the config file is the + // source of truth, so this disables any previously + // configured relay server. + name: "both_nil_disables_relay_server", + cfg: &ConfigVAlpha{}, + want: MaskedPrefs{ + RelayServerPortSet: true, + RelayServerStaticEndpointsSet: true, + }, + }, + { + // An empty (non-nil) static endpoints slice is treated as + // "advertise no static endpoints", disabling them. + name: "empty_static_endpoints_disables", + cfg: &ConfigVAlpha{ + RelayServerStaticEndpoints: []netip.AddrPort{}, + }, + want: MaskedPrefs{ + Prefs: Prefs{ + RelayServerStaticEndpoints: []netip.AddrPort{}, + }, + RelayServerPortSet: true, + RelayServerStaticEndpointsSet: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.cfg.ToPrefs() + if err != nil { + t.Fatalf("ToPrefs() unexpected error: %v", err) + } + // Compare only the relay-server-related fields rather than + // the whole MaskedPrefs, since ToPrefs populates many + // unrelated default fields (e.g. WantRunning). + if got.RelayServerPortSet != tt.want.RelayServerPortSet { + t.Errorf("RelayServerPortSet = %v; want %v", got.RelayServerPortSet, tt.want.RelayServerPortSet) + } + if got.RelayServerStaticEndpointsSet != tt.want.RelayServerStaticEndpointsSet { + t.Errorf("RelayServerStaticEndpointsSet = %v; want %v", got.RelayServerStaticEndpointsSet, tt.want.RelayServerStaticEndpointsSet) + } + if !reflect.DeepEqual(got.RelayServerPort, tt.want.RelayServerPort) { + t.Errorf("RelayServerPort = %v; want %v", got.RelayServerPort, tt.want.RelayServerPort) + } + if !slices.Equal(got.RelayServerStaticEndpoints, tt.want.RelayServerStaticEndpoints) { + t.Errorf("RelayServerStaticEndpoints = %v; want %v", got.RelayServerStaticEndpoints, tt.want.RelayServerStaticEndpoints) + } + }) + } +} diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index dbd0769d7..f62e11a47 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -7490,6 +7490,78 @@ func TestConfigFileReload(t *testing.T) { } }, }, + { + name: "enable_relay_server", + initial: &conffile.Config{ + Parsed: ipn.ConfigVAlpha{ + Version: "alpha0", + }, + }, + updated: &conffile.Config{ + Parsed: ipn.ConfigVAlpha{ + Version: "alpha0", + RelayServerPort: new(uint16(12345)), + RelayServerStaticEndpoints: []netip.AddrPort{netip.MustParseAddrPort("[2001:db8::1]:40000")}, + }, + }, + checkFn: func(t *testing.T, b *LocalBackend) { + pv := b.Prefs() + if port, ok := pv.RelayServerPort().GetOk(); !ok || port != 12345 { + t.Errorf("RelayServerPort = (%d, %v); want (12345, true)", port, ok) + } + if got := pv.RelayServerStaticEndpoints().AsSlice(); !slices.Equal(got, []netip.AddrPort{netip.MustParseAddrPort("[2001:db8::1]:40000")}) { + t.Errorf("RelayServerStaticEndpoints = %v; want [[2001:db8::1]:40000]", got) + } + }, + }, + { + // Disabling: a config that omits the relay server fields + // must clear any previously configured values, because + // ToPrefs sets the masks unconditionally. + name: "disable_relay_server", + initial: &conffile.Config{ + Parsed: ipn.ConfigVAlpha{ + Version: "alpha0", + RelayServerPort: new(uint16(12345)), + RelayServerStaticEndpoints: []netip.AddrPort{netip.MustParseAddrPort("[2001:db8::1]:40000")}, + }, + }, + updated: &conffile.Config{ + Parsed: ipn.ConfigVAlpha{ + Version: "alpha0", + }, + }, + checkFn: func(t *testing.T, b *LocalBackend) { + pv := b.Prefs() + if _, ok := pv.RelayServerPort().GetOk(); ok { + t.Errorf("RelayServerPort = %v; want disabled", pv.RelayServerPort()) + } + if got := pv.RelayServerStaticEndpoints().AsSlice(); len(got) != 0 { + t.Errorf("RelayServerStaticEndpoints = %v; want empty", got) + } + }, + }, + { + name: "change_relay_server_port", + initial: &conffile.Config{ + Parsed: ipn.ConfigVAlpha{ + Version: "alpha0", + RelayServerPort: new(uint16(12345)), + }, + }, + updated: &conffile.Config{ + Parsed: ipn.ConfigVAlpha{ + Version: "alpha0", + RelayServerPort: new(uint16(54321)), + }, + }, + checkFn: func(t *testing.T, b *LocalBackend) { + pv := b.Prefs() + if port, ok := pv.RelayServerPort().GetOk(); !ok || port != 54321 { + t.Errorf("RelayServerPort = (%d, %v); want (54321, true)", port, ok) + } + }, + }, } for _, tc := range tests {