net/udprelay: change ServerEndpoint time.Duration fields to tstime.GoDuration (#15725)

tstime.GoDuration JSON serializes with time.Duration.String(), which is
more human-friendly than nanoseconds.

ServerEndpoint is currently experimental, therefore breaking changes
are tolerable.

Updates tailscale/corp#27502

Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
Jordan Whited 2025-04-17 16:21:32 -07:00 committed by GitHub
parent aff8f1b358
commit 3a8a174308
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 103 additions and 6 deletions

View File

@ -21,6 +21,7 @@ import (
"go4.org/mem" "go4.org/mem"
"tailscale.com/disco" "tailscale.com/disco"
"tailscale.com/net/packet" "tailscale.com/net/packet"
"tailscale.com/tstime"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
@ -114,12 +115,12 @@ type ServerEndpoint struct {
// BindLifetime is amount of time post-allocation the Server will consider // BindLifetime is amount of time post-allocation the Server will consider
// the endpoint active while it has yet to be bound via 3-way bind handshake // the endpoint active while it has yet to be bound via 3-way bind handshake
// from both client parties. // from both client parties.
BindLifetime time.Duration BindLifetime tstime.GoDuration
// SteadyStateLifetime is the amount of time post 3-way bind handshake from // SteadyStateLifetime is the amount of time post 3-way bind handshake from
// both client parties the Server will consider the endpoint active lacking // both client parties the Server will consider the endpoint active lacking
// bidirectional data flow. // bidirectional data flow.
SteadyStateLifetime time.Duration SteadyStateLifetime tstime.GoDuration
} }
// serverEndpoint contains Server-internal ServerEndpoint state. serverEndpoint // serverEndpoint contains Server-internal ServerEndpoint state. serverEndpoint
@ -489,8 +490,8 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (ServerEndpoin
AddrPorts: s.addrPorts, AddrPorts: s.addrPorts,
VNI: e.vni, VNI: e.vni,
LamportID: e.lamportID, LamportID: e.lamportID,
BindLifetime: s.bindLifetime, BindLifetime: tstime.GoDuration{Duration: s.bindLifetime},
SteadyStateLifetime: s.steadyStateLifetime, SteadyStateLifetime: tstime.GoDuration{Duration: s.steadyStateLifetime},
}, nil }, nil
} }
// If an endpoint exists for the pair of key.DiscoPublic's, and is // If an endpoint exists for the pair of key.DiscoPublic's, and is
@ -526,7 +527,7 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (ServerEndpoin
AddrPorts: s.addrPorts, AddrPorts: s.addrPorts,
VNI: e.vni, VNI: e.vni,
LamportID: e.lamportID, LamportID: e.lamportID,
BindLifetime: s.bindLifetime, BindLifetime: tstime.GoDuration{Duration: s.bindLifetime},
SteadyStateLifetime: s.steadyStateLifetime, SteadyStateLifetime: tstime.GoDuration{Duration: s.steadyStateLifetime},
}, nil }, nil
} }

View File

@ -5,6 +5,8 @@ package udprelay
import ( import (
"bytes" "bytes"
"encoding/json"
"math"
"net" "net"
"net/netip" "net/netip"
"testing" "testing"
@ -15,6 +17,7 @@ import (
"go4.org/mem" "go4.org/mem"
"tailscale.com/disco" "tailscale.com/disco"
"tailscale.com/net/packet" "tailscale.com/net/packet"
"tailscale.com/tstime"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
@ -202,3 +205,96 @@ func TestServer(t *testing.T) {
t.Fatal("unexpected msg B->A") t.Fatal("unexpected msg B->A")
} }
} }
func TestServerEndpointJSONUnmarshal(t *testing.T) {
tests := []struct {
name string
json []byte
wantErr bool
}{
{
name: "valid",
json: []byte(`{"ServerDisco":"discokey:003cd7453e04a653eb0e7a18f206fc353180efadb2facfd05ebd6982a1392c7f","LamportID":18446744073709551615,"AddrPorts":["127.0.0.1:1","127.0.0.2:2"],"VNI":16777215,"BindLifetime":"30s","SteadyStateLifetime":"5m0s"}`),
wantErr: false,
},
{
name: "invalid ServerDisco",
json: []byte(`{"ServerDisco":"1","LamportID":18446744073709551615,"AddrPorts":["127.0.0.1:1","127.0.0.2:2"],"VNI":16777215,"BindLifetime":"30s","SteadyStateLifetime":"5m0s"}`),
wantErr: true,
},
{
name: "invalid LamportID",
json: []byte(`{"ServerDisco":"discokey:003cd7453e04a653eb0e7a18f206fc353180efadb2facfd05ebd6982a1392c7f","LamportID":1.1,"AddrPorts":["127.0.0.1:1","127.0.0.2:2"],"VNI":16777215,"BindLifetime":"30s","SteadyStateLifetime":"5m0s"}`),
wantErr: true,
},
{
name: "invalid AddrPorts",
json: []byte(`{"ServerDisco":"discokey:003cd7453e04a653eb0e7a18f206fc353180efadb2facfd05ebd6982a1392c7f","LamportID":18446744073709551615,"AddrPorts":["127.0.0.1.1:1","127.0.0.2:2"],"VNI":16777215,"BindLifetime":"30s","SteadyStateLifetime":"5m0s"}`),
wantErr: true,
},
{
name: "invalid VNI",
json: []byte(`{"ServerDisco":"discokey:003cd7453e04a653eb0e7a18f206fc353180efadb2facfd05ebd6982a1392c7f","LamportID":18446744073709551615,"AddrPorts":["127.0.0.1:1","127.0.0.2:2"],"VNI":18446744073709551615,"BindLifetime":"30s","SteadyStateLifetime":"5m0s"}`),
wantErr: true,
},
{
name: "invalid BindLifetime",
json: []byte(`{"ServerDisco":"discokey:003cd7453e04a653eb0e7a18f206fc353180efadb2facfd05ebd6982a1392c7f","LamportID":18446744073709551615,"AddrPorts":["127.0.0.1:1","127.0.0.2:2"],"VNI":16777215,"BindLifetime":"5","SteadyStateLifetime":"5m0s"}`),
wantErr: true,
},
{
name: "invalid SteadyStateLifetime",
json: []byte(`{"ServerDisco":"discokey:003cd7453e04a653eb0e7a18f206fc353180efadb2facfd05ebd6982a1392c7f","LamportID":18446744073709551615,"AddrPorts":["127.0.0.1:1","127.0.0.2:2"],"VNI":16777215,"BindLifetime":"30s","SteadyStateLifetime":"5"}`),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out ServerEndpoint
err := json.Unmarshal(tt.json, &out)
if tt.wantErr != (err != nil) {
t.Fatalf("wantErr: %v (err == nil): %v", tt.wantErr, err == nil)
}
if tt.wantErr {
return
}
})
}
}
func TestServerEndpointJSONMarshal(t *testing.T) {
tests := []struct {
name string
serverEndpoint ServerEndpoint
}{
{
name: "valid roundtrip",
serverEndpoint: ServerEndpoint{
ServerDisco: key.NewDisco().Public(),
LamportID: uint64(math.MaxUint64),
AddrPorts: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:1"), netip.MustParseAddrPort("127.0.0.2:2")},
VNI: 1<<24 - 1,
BindLifetime: tstime.GoDuration{Duration: defaultBindLifetime},
SteadyStateLifetime: tstime.GoDuration{Duration: defaultSteadyStateLifetime},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := json.Marshal(&tt.serverEndpoint)
if err != nil {
t.Fatal(err)
}
var got ServerEndpoint
err = json.Unmarshal(b, &got)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, tt.serverEndpoint, cmpopts.EquateComparable(netip.AddrPort{}, key.DiscoPublic{})); diff != "" {
t.Fatalf("ServerEndpoint unequal (-got +want)\n%s", diff)
}
})
}
}