Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
df03e6e430 |
|
@ -946,21 +946,6 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
|
||||||
// in url and returns information extracted from it.
|
|
||||||
func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
|
|
||||||
vr := struct {
|
|
||||||
URL string
|
|
||||||
}{url}
|
|
||||||
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeJSON[*tka.DeeplinkValidationResult](body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServeConfig sets or replaces the serving settings.
|
// SetServeConfig sets or replaces the serving settings.
|
||||||
// If config is nil, settings are cleared and serving is disabled.
|
// If config is nil, settings are cleared and serving is disabled.
|
||||||
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||||
|
|
|
@ -72,7 +72,7 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||||
return nil, fmt.Errorf("can not load cert: %w", err)
|
return nil, fmt.Errorf("can not load cert: %w", err)
|
||||||
}
|
}
|
||||||
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
||||||
// return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
||||||
}
|
}
|
||||||
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
|
||||||
|
|
||||||
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
if hi.ServerName != m.hostname {
|
if hi.ServerName != m.hostname {
|
||||||
//return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
|
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a shallow copy of the cert so the caller can append to its
|
// Return a shallow copy of the cert so the caller can append to its
|
||||||
|
|
|
@ -182,7 +182,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||||
golang.org/x/exp/maps from tailscale.com/types/views
|
|
||||||
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
||||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||||
golang.org/x/net/dns/dnsmessage from net+
|
golang.org/x/net/dns/dnsmessage from net+
|
||||||
|
|
|
@ -726,7 +726,8 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||||
// the health check, rather than just a string.
|
// the health check, rather than just a string.
|
||||||
func upWorthyWarning(s string) bool {
|
func upWorthyWarning(s string) bool {
|
||||||
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
|
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
|
||||||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff)
|
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
|
||||||
|
strings.Contains(s, healthmsg.LockedOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUpWarnings(ctx context.Context) {
|
func checkUpWarnings(ctx context.Context) {
|
||||||
|
|
|
@ -173,7 +173,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||||
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
|
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
|
||||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||||
golang.org/x/exp/maps from tailscale.com/types/views
|
|
||||||
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
||||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||||
golang.org/x/net/dns/dnsmessage from net+
|
golang.org/x/net/dns/dnsmessage from net+
|
||||||
|
|
|
@ -379,7 +379,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||||
golang.org/x/exp/constraints from golang.org/x/exp/slices+
|
golang.org/x/exp/constraints from golang.org/x/exp/slices+
|
||||||
golang.org/x/exp/maps from tailscale.com/wgengine+
|
golang.org/x/exp/maps from tailscale.com/wgengine
|
||||||
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
|
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
|
||||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||||
golang.org/x/net/dns/dnsmessage from net+
|
golang.org/x/net/dns/dnsmessage from net+
|
||||||
|
|
|
@ -33,8 +33,6 @@ type testAttempt struct {
|
||||||
outcome string // "pass", "fail", "skip"
|
outcome string // "pass", "fail", "skip"
|
||||||
logs bytes.Buffer
|
logs bytes.Buffer
|
||||||
isMarkedFlaky bool // set if the test is marked as flaky
|
isMarkedFlaky bool // set if the test is marked as flaky
|
||||||
|
|
||||||
pkgFinished bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type testName struct {
|
type testName struct {
|
||||||
|
@ -61,12 +59,7 @@ type goTestOutput struct {
|
||||||
|
|
||||||
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||||
|
|
||||||
// runTests runs the tests in pt and sends the results on ch. It sends a
|
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string) []*testAttempt {
|
||||||
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
|
|
||||||
// set to true.
|
|
||||||
// It calls close(ch) when it's done.
|
|
||||||
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
|
|
||||||
defer close(ch)
|
|
||||||
args := []string{"test", "-json", pt.pattern}
|
args := []string{"test", "-json", pt.pattern}
|
||||||
args = append(args, otherArgs...)
|
args = append(args, otherArgs...)
|
||||||
if len(pt.tests) > 0 {
|
if len(pt.tests) > 0 {
|
||||||
|
@ -98,6 +91,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
||||||
|
|
||||||
jd := json.NewDecoder(r)
|
jd := json.NewDecoder(r)
|
||||||
resultMap := make(map[testName]*testAttempt)
|
resultMap := make(map[testName]*testAttempt)
|
||||||
|
var out []*testAttempt
|
||||||
for {
|
for {
|
||||||
var goOutput goTestOutput
|
var goOutput goTestOutput
|
||||||
if err := jd.Decode(&goOutput); err != nil {
|
if err := jd.Decode(&goOutput); err != nil {
|
||||||
|
@ -107,16 +101,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if goOutput.Test == "" {
|
if goOutput.Test == "" {
|
||||||
switch goOutput.Action {
|
|
||||||
case "fail", "pass", "skip":
|
|
||||||
ch <- &testAttempt{
|
|
||||||
name: testName{
|
|
||||||
pkg: goOutput.Package,
|
|
||||||
},
|
|
||||||
outcome: goOutput.Action,
|
|
||||||
pkgFinished: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := testName{
|
name := testName{
|
||||||
|
@ -139,7 +123,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
||||||
}
|
}
|
||||||
case "skip", "pass", "fail":
|
case "skip", "pass", "fail":
|
||||||
resultMap[name].outcome = goOutput.Action
|
resultMap[name].outcome = goOutput.Action
|
||||||
ch <- resultMap[name]
|
out = append(out, resultMap[name])
|
||||||
case "output":
|
case "output":
|
||||||
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
|
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
|
||||||
resultMap[name].isMarkedFlaky = true
|
resultMap[name].isMarkedFlaky = true
|
||||||
|
@ -149,6 +133,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<-done
|
<-done
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -189,90 +174,58 @@ func main() {
|
||||||
}
|
}
|
||||||
pattern, otherArgs := args[0], args[1:]
|
pattern, otherArgs := args[0], args[1:]
|
||||||
|
|
||||||
type nextRun struct {
|
toRun := []*packageTests{ // packages still to test
|
||||||
tests []*packageTests
|
{pattern: pattern},
|
||||||
attempt int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toRun := []*nextRun{
|
pkgAttempts := make(map[string]int) // tracks how many times we've tried a package
|
||||||
{
|
|
||||||
tests: []*packageTests{{pattern: pattern}},
|
|
||||||
attempt: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
printPkgOutcome := func(pkg, outcome string, attempt int) {
|
|
||||||
if outcome == "skip" {
|
|
||||||
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if outcome == "pass" {
|
|
||||||
outcome = "ok"
|
|
||||||
}
|
|
||||||
if outcome == "fail" {
|
|
||||||
outcome = "FAIL"
|
|
||||||
}
|
|
||||||
if attempt > 1 {
|
|
||||||
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\t%s\n", outcome, pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
attempt := 0
|
||||||
for len(toRun) > 0 {
|
for len(toRun) > 0 {
|
||||||
var thisRun *nextRun
|
attempt++
|
||||||
thisRun, toRun = toRun[0], toRun[1:]
|
var pt *packageTests
|
||||||
|
pt, toRun = toRun[0], toRun[1:]
|
||||||
|
|
||||||
if thisRun.attempt >= maxAttempts {
|
toRetry := make(map[string][]string) // pkg -> tests to retry
|
||||||
fmt.Println("max attempts reached")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if thisRun.attempt > 1 {
|
|
||||||
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
|
|
||||||
}
|
|
||||||
|
|
||||||
failed := false
|
failed := false
|
||||||
toRetry := make(map[string][]string) // pkg -> tests to retry
|
for _, tr := range runTests(ctx, attempt, pt, otherArgs) {
|
||||||
for _, pt := range thisRun.tests {
|
if *v || tr.outcome == "fail" {
|
||||||
ch := make(chan *testAttempt)
|
io.Copy(os.Stderr, &tr.logs)
|
||||||
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
|
}
|
||||||
for tr := range ch {
|
if tr.outcome != "fail" {
|
||||||
if tr.pkgFinished {
|
continue
|
||||||
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
|
}
|
||||||
continue
|
if tr.isMarkedFlaky {
|
||||||
}
|
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
|
||||||
if *v || tr.outcome == "fail" {
|
} else {
|
||||||
io.Copy(os.Stdout, &tr.logs)
|
failed = true
|
||||||
}
|
|
||||||
if tr.outcome != "fail" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tr.isMarkedFlaky {
|
|
||||||
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
|
|
||||||
} else {
|
|
||||||
failed = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if failed {
|
if failed {
|
||||||
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if len(toRetry) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pkgs := maps.Keys(toRetry)
|
pkgs := maps.Keys(toRetry)
|
||||||
sort.Strings(pkgs)
|
sort.Strings(pkgs)
|
||||||
nextRun := &nextRun{
|
|
||||||
attempt: thisRun.attempt + 1,
|
|
||||||
}
|
|
||||||
for _, pkg := range pkgs {
|
for _, pkg := range pkgs {
|
||||||
tests := toRetry[pkg]
|
tests := toRetry[pkg]
|
||||||
sort.Strings(tests)
|
sort.Strings(tests)
|
||||||
nextRun.tests = append(nextRun.tests, &packageTests{
|
pkgAttempts[pkg]++
|
||||||
|
if pkgAttempts[pkg] >= maxAttempts {
|
||||||
|
fmt.Println("Too many attempts for flaky tests:", pkg, tests)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println("\nRetrying flaky tests:", pkg, tests)
|
||||||
|
toRun = append(toRun, &packageTests{
|
||||||
pattern: pkg,
|
pattern: pkg,
|
||||||
tests: tests,
|
tests: tests,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
toRun = append(toRun, nextRun)
|
|
||||||
}
|
}
|
||||||
|
for _, a := range pkgAttempts {
|
||||||
|
if a >= maxAttempts {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("PASS")
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,25 +287,6 @@ func (nc *NoiseClient) GetSingleUseRoundTripper(ctx context.Context) (http.Round
|
||||||
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
|
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
// contextErr is an error that wraps another error and is used to indicate that
|
|
||||||
// the error was because a context expired.
|
|
||||||
type contextErr struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e contextErr) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e contextErr) Unwrap() error {
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// getConn returns a noiseConn that can be used to make requests to the
|
|
||||||
// coordination server. It may return a cached connection or create a new one.
|
|
||||||
// Dials are singleflighted, so concurrent calls to getConn may only dial once.
|
|
||||||
// As such, context values may not be respected as there are no guarantees that
|
|
||||||
// the context passed to getConn is the same as the context passed to dial.
|
|
||||||
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
|
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
|
||||||
nc.mu.Lock()
|
nc.mu.Lock()
|
||||||
if last := nc.last; last != nil && last.canTakeNewRequest() {
|
if last := nc.last; last != nil && last.canTakeNewRequest() {
|
||||||
|
@ -314,35 +295,11 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
|
||||||
}
|
}
|
||||||
nc.mu.Unlock()
|
nc.mu.Unlock()
|
||||||
|
|
||||||
for {
|
conn, err, _ := nc.sfDial.Do(struct{}{}, nc.dial)
|
||||||
// We singeflight the dial to avoid making multiple connections, however
|
if err != nil {
|
||||||
// that means that we can't simply cancel the dial if the context is
|
return nil, err
|
||||||
// canceled. Instead, we have to additionally check that the context
|
|
||||||
// which was canceled is our context and retry if our context is still
|
|
||||||
// valid.
|
|
||||||
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*noiseConn, error) {
|
|
||||||
c, err := nc.dial(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return nil, contextErr{ctx.Err()}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
})
|
|
||||||
var ce contextErr
|
|
||||||
if err == nil || !errors.As(err, &ce) {
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
if ctx.Err() == nil {
|
|
||||||
// The dial failed because of a context error, but our context
|
|
||||||
// is still valid. Retry.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// The dial failed because our context was canceled. Return the
|
|
||||||
// underlying error.
|
|
||||||
return nil, ce.Unwrap()
|
|
||||||
}
|
}
|
||||||
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
@ -387,7 +344,7 @@ func (nc *NoiseClient) Close() error {
|
||||||
|
|
||||||
// dial opens a new connection to tailcontrol, fetching the server noise key
|
// dial opens a new connection to tailcontrol, fetching the server noise key
|
||||||
// if not cached.
|
// if not cached.
|
||||||
func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
func (nc *NoiseClient) dial() (*noiseConn, error) {
|
||||||
nc.mu.Lock()
|
nc.mu.Lock()
|
||||||
connID := nc.nextID
|
connID := nc.nextID
|
||||||
nc.nextID++
|
nc.nextID++
|
||||||
|
@ -435,7 +392,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
clientConn, err := (&controlhttp.Dialer{
|
clientConn, err := (&controlhttp.Dialer{
|
||||||
|
|
|
@ -6,20 +6,22 @@ SA_NAME ?= tailscale
|
||||||
TS_KUBE_SECRET ?= tailscale
|
TS_KUBE_SECRET ?= tailscale
|
||||||
|
|
||||||
rbac:
|
rbac:
|
||||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml
|
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||||
@echo "---"
|
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
|
||||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml
|
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
|
||||||
@echo "---"
|
|
||||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml
|
|
||||||
|
|
||||||
sidecar:
|
sidecar:
|
||||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g"
|
@kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
|
||||||
|
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
|
||||||
|
|
||||||
userspace-sidecar:
|
userspace-sidecar:
|
||||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g"
|
@kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
|
||||||
|
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g"
|
kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
|
||||||
|
sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" | kubectl create -f-
|
||||||
|
|
||||||
subnet-router:
|
subnet-router:
|
||||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g"
|
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
|
||||||
|
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" | kubectl create -f-
|
||||||
|
|
|
@ -26,7 +26,7 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
|
||||||
```bash
|
```bash
|
||||||
export SA_NAME=tailscale
|
export SA_NAME=tailscale
|
||||||
export TS_KUBE_SECRET=tailscale-auth
|
export TS_KUBE_SECRET=tailscale-auth
|
||||||
make rbac | kubectl apply -f-
|
make rbac
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sample Sidecar
|
### Sample Sidecar
|
||||||
|
@ -36,7 +36,7 @@ Running as a sidecar allows you to directly expose a Kubernetes pod over Tailsca
|
||||||
1. Create and login to the sample nginx pod with a Tailscale sidecar
|
1. Create and login to the sample nginx pod with a Tailscale sidecar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make sidecar | kubectl apply -f-
|
make sidecar
|
||||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||||
kubectl logs nginx ts-sidecar
|
kubectl logs nginx ts-sidecar
|
||||||
```
|
```
|
||||||
|
@ -60,7 +60,7 @@ You can also run the sidecar in userspace mode. The obvious benefit is reducing
|
||||||
1. Create and login to the sample nginx pod with a Tailscale sidecar
|
1. Create and login to the sample nginx pod with a Tailscale sidecar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make userspace-sidecar | kubectl apply -f-
|
make userspace-sidecar
|
||||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||||
kubectl logs nginx ts-sidecar
|
kubectl logs nginx ts-sidecar
|
||||||
```
|
```
|
||||||
|
@ -100,7 +100,7 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
|
||||||
1. Deploy the proxy pod
|
1. Deploy the proxy pod
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make proxy | kubectl apply -f-
|
make proxy
|
||||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||||
kubectl logs proxy
|
kubectl logs proxy
|
||||||
```
|
```
|
||||||
|
@ -133,7 +133,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
|
||||||
1. Deploy the subnet-router pod.
|
1. Deploy the subnet-router pod.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make subnet-router | kubectl apply -f-
|
make subnet-router
|
||||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||||
kubectl logs subnet-router
|
kubectl logs subnet-router
|
||||||
```
|
```
|
||||||
|
|
|
@ -10,4 +10,5 @@ package healthmsg
|
||||||
const (
|
const (
|
||||||
WarnAcceptRoutesOff = "Some peers are advertising routes but --accept-routes is false"
|
WarnAcceptRoutesOff = "Some peers are advertising routes but --accept-routes is false"
|
||||||
TailscaleSSHOnBut = "Tailscale SSH enabled, but " // + ... something from caller
|
TailscaleSSHOnBut = "Tailscale SSH enabled, but " // + ... something from caller
|
||||||
|
LockedOut = "this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"
|
||||||
)
|
)
|
||||||
|
|
|
@ -742,6 +742,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||||
HostName: p.Hostinfo.Hostname(),
|
HostName: p.Hostinfo.Hostname(),
|
||||||
DNSName: p.Name,
|
DNSName: p.Name,
|
||||||
OS: p.Hostinfo.OS(),
|
OS: p.Hostinfo.OS(),
|
||||||
|
KeepAlive: p.KeepAlive,
|
||||||
LastSeen: lastSeen,
|
LastSeen: lastSeen,
|
||||||
Online: p.Online != nil && *p.Online,
|
Online: p.Online != nil && *p.Online,
|
||||||
ShareeNode: p.Hostinfo.ShareeNode(),
|
ShareeNode: p.Hostinfo.ShareeNode(),
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
|
"tailscale.com/health/healthmsg"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
|
@ -124,7 +125,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||||
|
|
||||||
// Check that we ourselves are not locked out, report a health issue if so.
|
// Check that we ourselves are not locked out, report a health issue if so.
|
||||||
if nm.SelfNode != nil && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key, nm.SelfNode.KeySignature) != nil {
|
if nm.SelfNode != nil && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key, nm.SelfNode.KeySignature) != nil {
|
||||||
health.SetTKAHealth(errors.New("this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"))
|
health.SetTKAHealth(errors.New(healthmsg.LockedOut))
|
||||||
} else {
|
} else {
|
||||||
health.SetTKAHealth(nil)
|
health.SetTKAHealth(nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,8 +223,9 @@ type PeerStatus struct {
|
||||||
LastSeen time.Time // last seen to tailcontrol; only present if offline
|
LastSeen time.Time // last seen to tailcontrol; only present if offline
|
||||||
LastHandshake time.Time // with local wireguard
|
LastHandshake time.Time // with local wireguard
|
||||||
Online bool // whether node is connected to the control plane
|
Online bool // whether node is connected to the control plane
|
||||||
ExitNode bool // true if this is the currently selected exit node.
|
KeepAlive bool
|
||||||
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
ExitNode bool // true if this is the currently selected exit node.
|
||||||
|
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
||||||
|
|
||||||
// Active is whether the node was recently active. The
|
// Active is whether the node was recently active. The
|
||||||
// definition is somewhat undefined but has historically and
|
// definition is somewhat undefined but has historically and
|
||||||
|
@ -436,6 +437,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||||
if st.InEngine {
|
if st.InEngine {
|
||||||
e.InEngine = true
|
e.InEngine = true
|
||||||
}
|
}
|
||||||
|
if st.KeepAlive {
|
||||||
|
e.KeepAlive = true
|
||||||
|
}
|
||||||
if st.ExitNode {
|
if st.ExitNode {
|
||||||
e.ExitNode = true
|
e.ExitNode = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,14 +45,6 @@ func (m *LabelMap) Get(key string) *expvar.Int {
|
||||||
return m.Map.Get(key).(*expvar.Int)
|
return m.Map.Get(key).(*expvar.Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIncrFunc returns a function that increments the expvar.Int named by key.
|
|
||||||
//
|
|
||||||
// Most callers should not need this; it exists to satisfy an
|
|
||||||
// interface elsewhere.
|
|
||||||
func (m *LabelMap) GetIncrFunc(key string) func(delta int64) {
|
|
||||||
return m.Get(key).Add
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFloat returns a direct pointer to the expvar.Float for key, creating it
|
// GetFloat returns a direct pointer to the expvar.Float for key, creating it
|
||||||
// if necessary.
|
// if necessary.
|
||||||
func (m *LabelMap) GetFloat(key string) *expvar.Float {
|
func (m *LabelMap) GetFloat(key string) *expvar.Float {
|
||||||
|
|
|
@ -11,18 +11,6 @@ import (
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLabelMap(t *testing.T) {
|
|
||||||
var m LabelMap
|
|
||||||
m.GetIncrFunc("foo")(1)
|
|
||||||
m.GetIncrFunc("bar")(2)
|
|
||||||
if g, w := m.Get("foo").Value(), int64(1); g != w {
|
|
||||||
t.Errorf("foo = %v; want %v", g, w)
|
|
||||||
}
|
|
||||||
if g, w := m.Get("bar").Value(), int64(2); g != w {
|
|
||||||
t.Errorf("bar = %v; want %v", g, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCurrentFileDescriptors(t *testing.T) {
|
func TestCurrentFileDescriptors(t *testing.T) {
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
t.Skipf("skipping on %v", runtime.GOOS)
|
t.Skipf("skipping on %v", runtime.GOOS)
|
||||||
|
|
|
@ -36,9 +36,9 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResolver(tb testing.TB) *Resolver {
|
func newResolver(tb testing.TB) *Resolver {
|
||||||
clock := tstest.NewClock(tstest.ClockOpts{
|
clock := &tstest.Clock{
|
||||||
Step: 50 * time.Millisecond,
|
Step: 50 * time.Millisecond,
|
||||||
})
|
}
|
||||||
return &Resolver{
|
return &Resolver{
|
||||||
Logf: tb.Logf,
|
Logf: tb.Logf,
|
||||||
timeNow: clock.Now,
|
timeNow: clock.Now,
|
||||||
|
|
|
@ -18,9 +18,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageCache(t *testing.T) {
|
func TestMessageCache(t *testing.T) {
|
||||||
clock := tstest.NewClock(tstest.ClockOpts{
|
clock := &tstest.Clock{
|
||||||
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
|
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
|
||||||
})
|
}
|
||||||
mc := &MessageCache{Clock: clock.Now}
|
mc := &MessageCache{Clock: clock.Now}
|
||||||
mc.SetMaxCacheSize(2)
|
mc.SetMaxCacheSize(2)
|
||||||
clock.Advance(time.Second)
|
clock.Advance(time.Second)
|
||||||
|
|
|
@ -184,17 +184,6 @@ func (b *Build) TmpDir() string {
|
||||||
// binary. Builds are cached by path and env, so each build only happens once
|
// binary. Builds are cached by path and env, so each build only happens once
|
||||||
// per process execution.
|
// per process execution.
|
||||||
func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error) {
|
func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error) {
|
||||||
return b.BuildGoBinaryWithTags(path, env, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildGoBinaryWithTags builds the Go binary at path and returns the
|
|
||||||
// path to the binary. Builds are cached by path, env and tags, so
|
|
||||||
// each build only happens once per process execution.
|
|
||||||
//
|
|
||||||
// The passed in tags override gocross's automatic selection of build
|
|
||||||
// tags, so you will have to figure out and specify all the tags
|
|
||||||
// relevant to your build.
|
|
||||||
func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags []string) (string, error) {
|
|
||||||
err := b.Once("init-go", func() error {
|
err := b.Once("init-go", func() error {
|
||||||
log.Printf("Initializing Go toolchain")
|
log.Printf("Initializing Go toolchain")
|
||||||
// If the build is using a tool/go, it may need to download a toolchain
|
// If the build is using a tool/go, it may need to download a toolchain
|
||||||
|
@ -208,7 +197,7 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
buildKey := []any{"go-build", path, env, tags}
|
buildKey := []any{"go-build", path, env}
|
||||||
return b.goBuilds.Do(buildKey, func() (string, error) {
|
return b.goBuilds.Do(buildKey, func() (string, error) {
|
||||||
b.goBuildLimit <- struct{}{}
|
b.goBuildLimit <- struct{}{}
|
||||||
defer func() { <-b.goBuildLimit }()
|
defer func() { <-b.goBuildLimit }()
|
||||||
|
@ -218,17 +207,9 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
|
||||||
envStrs = append(envStrs, k+"="+v)
|
envStrs = append(envStrs, k+"="+v)
|
||||||
}
|
}
|
||||||
sort.Strings(envStrs)
|
sort.Strings(envStrs)
|
||||||
|
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " "))
|
||||||
buildDir := b.TmpDir()
|
buildDir := b.TmpDir()
|
||||||
args := []string{"build", "-v", "-o", buildDir}
|
cmd := b.Command(b.Repo, b.Go, "build", "-v", "-o", buildDir, path)
|
||||||
if len(tags) > 0 {
|
|
||||||
tagsStr := strings.Join(tags, ",")
|
|
||||||
log.Printf("Building %s (with env %s, tags %s)", path, strings.Join(envStrs, " "), tagsStr)
|
|
||||||
args = append(args, "-tags="+tagsStr)
|
|
||||||
} else {
|
|
||||||
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " "))
|
|
||||||
}
|
|
||||||
args = append(args, path)
|
|
||||||
cmd := b.Command(b.Repo, b.Go, args...)
|
|
||||||
for k, v := range env {
|
for k, v := range env {
|
||||||
cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v)
|
cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,6 +242,8 @@ type Node struct {
|
||||||
// current node doesn't have permission to know.
|
// current node doesn't have permission to know.
|
||||||
Online *bool `json:",omitempty"`
|
Online *bool `json:",omitempty"`
|
||||||
|
|
||||||
|
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
|
||||||
|
|
||||||
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
|
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
|
||||||
|
|
||||||
// Capabilities are capabilities that the node has.
|
// Capabilities are capabilities that the node has.
|
||||||
|
@ -1282,7 +1284,7 @@ type DNSConfig struct {
|
||||||
// match.
|
// match.
|
||||||
//
|
//
|
||||||
// Matches are case insensitive.
|
// Matches are case insensitive.
|
||||||
ExitNodeFilteredSet []string `json:",omitempty"`
|
ExitNodeFilteredSet []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSRecord is an extra DNS record to add to MagicDNS.
|
// DNSRecord is an extra DNS record to add to MagicDNS.
|
||||||
|
|
|
@ -93,6 +93,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
||||||
PrimaryRoutes []netip.Prefix
|
PrimaryRoutes []netip.Prefix
|
||||||
LastSeen *time.Time
|
LastSeen *time.Time
|
||||||
Online *bool
|
Online *bool
|
||||||
|
KeepAlive bool
|
||||||
MachineAuthorized bool
|
MachineAuthorized bool
|
||||||
Capabilities []string
|
Capabilities []string
|
||||||
UnsignedPeerAPIOnly bool
|
UnsignedPeerAPIOnly bool
|
||||||
|
|
|
@ -347,7 +347,7 @@ func TestNodeEqual(t *testing.T) {
|
||||||
"Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey",
|
"Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey",
|
||||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||||
"Created", "Cap", "Tags", "PrimaryRoutes",
|
"Created", "Cap", "Tags", "PrimaryRoutes",
|
||||||
"LastSeen", "Online", "MachineAuthorized",
|
"LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||||
"Capabilities",
|
"Capabilities",
|
||||||
"UnsignedPeerAPIOnly",
|
"UnsignedPeerAPIOnly",
|
||||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||||
|
|
|
@ -168,6 +168,7 @@ func (v NodeView) Online() *bool {
|
||||||
return &x
|
return &x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive }
|
||||||
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||||
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
||||||
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
|
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
|
||||||
|
@ -209,6 +210,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
||||||
PrimaryRoutes []netip.Prefix
|
PrimaryRoutes []netip.Prefix
|
||||||
LastSeen *time.Time
|
LastSeen *time.Time
|
||||||
Online *bool
|
Online *bool
|
||||||
|
KeepAlive bool
|
||||||
MachineAuthorized bool
|
MachineAuthorized bool
|
||||||
Capabilities []string
|
Capabilities []string
|
||||||
UnsignedPeerAPIOnly bool
|
UnsignedPeerAPIOnly bool
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package heap provides heap operations for any type that implements
|
|
||||||
// heap.Interface. A heap is a tree with the property that each node is the
|
|
||||||
// minimum-valued node in its subtree.
|
|
||||||
//
|
|
||||||
// The minimum element in the tree is the root, at index 0.
|
|
||||||
//
|
|
||||||
// A heap is a common way to implement a priority queue. To build a priority
|
|
||||||
// queue, implement the Heap interface with the (negative) priority as the
|
|
||||||
// ordering for the Less method, so Push adds items while Pop removes the
|
|
||||||
// highest-priority item from the queue. The Examples include such an
|
|
||||||
// implementation; the file example_pq_test.go has the complete source.
|
|
||||||
//
|
|
||||||
// This package is a copy of the Go standard library's
|
|
||||||
// container/heap, but using generics.
|
|
||||||
package heap
|
|
||||||
|
|
||||||
import "sort"
|
|
||||||
|
|
||||||
// The Interface type describes the requirements
|
|
||||||
// for a type using the routines in this package.
|
|
||||||
// Any type that implements it may be used as a
|
|
||||||
// min-heap with the following invariants (established after
|
|
||||||
// Init has been called or if the data is empty or sorted):
|
|
||||||
//
|
|
||||||
// !h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
|
|
||||||
//
|
|
||||||
// Note that Push and Pop in this interface are for package heap's
|
|
||||||
// implementation to call. To add and remove things from the heap,
|
|
||||||
// use heap.Push and heap.Pop.
|
|
||||||
type Interface[V any] interface {
|
|
||||||
sort.Interface
|
|
||||||
Push(x V) // add x as element Len()
|
|
||||||
Pop() V // remove and return element Len() - 1.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init establishes the heap invariants required by the other routines in this package.
|
|
||||||
// Init is idempotent with respect to the heap invariants
|
|
||||||
// and may be called whenever the heap invariants may have been invalidated.
|
|
||||||
// The complexity is O(n) where n = h.Len().
|
|
||||||
func Init[V any](h Interface[V]) {
|
|
||||||
// heapify
|
|
||||||
n := h.Len()
|
|
||||||
for i := n/2 - 1; i >= 0; i-- {
|
|
||||||
down(h, i, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push pushes the element x onto the heap.
|
|
||||||
// The complexity is O(log n) where n = h.Len().
|
|
||||||
func Push[V any](h Interface[V], x V) {
|
|
||||||
h.Push(x)
|
|
||||||
up(h, h.Len()-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop removes and returns the minimum element (according to Less) from the heap.
|
|
||||||
// The complexity is O(log n) where n = h.Len().
|
|
||||||
// Pop is equivalent to Remove(h, 0).
|
|
||||||
func Pop[V any](h Interface[V]) V {
|
|
||||||
n := h.Len() - 1
|
|
||||||
h.Swap(0, n)
|
|
||||||
down(h, 0, n)
|
|
||||||
return h.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes and returns the element at index i from the heap.
|
|
||||||
// The complexity is O(log n) where n = h.Len().
|
|
||||||
func Remove[V any](h Interface[V], i int) V {
|
|
||||||
n := h.Len() - 1
|
|
||||||
if n != i {
|
|
||||||
h.Swap(i, n)
|
|
||||||
if !down(h, i, n) {
|
|
||||||
up(h, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix re-establishes the heap ordering after the element at index i has changed its value.
|
|
||||||
// Changing the value of the element at index i and then calling Fix is equivalent to,
|
|
||||||
// but less expensive than, calling Remove(h, i) followed by a Push of the new value.
|
|
||||||
// The complexity is O(log n) where n = h.Len().
|
|
||||||
func Fix[V any](h Interface[V], i int) {
|
|
||||||
if !down(h, i, h.Len()) {
|
|
||||||
up(h, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func up[V any](h Interface[V], j int) {
|
|
||||||
for {
|
|
||||||
i := (j - 1) / 2 // parent
|
|
||||||
if i == j || !h.Less(j, i) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h.Swap(i, j)
|
|
||||||
j = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func down[V any](h Interface[V], i0, n int) bool {
|
|
||||||
i := i0
|
|
||||||
for {
|
|
||||||
j1 := 2*i + 1
|
|
||||||
if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
|
|
||||||
break
|
|
||||||
}
|
|
||||||
j := j1 // left child
|
|
||||||
if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
|
|
||||||
j = j2 // = 2*i + 2 // right child
|
|
||||||
}
|
|
||||||
if !h.Less(j, i) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h.Swap(i, j)
|
|
||||||
i = j
|
|
||||||
}
|
|
||||||
return i > i0
|
|
||||||
}
|
|
|
@ -1,216 +0,0 @@
|
||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package heap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
|
||||||
|
|
||||||
type myHeap[T constraints.Ordered] []T
|
|
||||||
|
|
||||||
func (h *myHeap[T]) Less(i, j int) bool {
|
|
||||||
return (*h)[i] < (*h)[j]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *myHeap[T]) Swap(i, j int) {
|
|
||||||
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *myHeap[T]) Len() int {
|
|
||||||
return len(*h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *myHeap[T]) Pop() (v T) {
|
|
||||||
*h, v = (*h)[:h.Len()-1], (*h)[h.Len()-1]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *myHeap[T]) Push(v T) {
|
|
||||||
*h = append(*h, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h myHeap[T]) verify(t *testing.T, i int) {
|
|
||||||
t.Helper()
|
|
||||||
n := h.Len()
|
|
||||||
j1 := 2*i + 1
|
|
||||||
j2 := 2*i + 2
|
|
||||||
if j1 < n {
|
|
||||||
if h.Less(j1, i) {
|
|
||||||
t.Errorf("heap invariant invalidated [%d] = %v > [%d] = %v", i, h[i], j1, h[j1])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.verify(t, j1)
|
|
||||||
}
|
|
||||||
if j2 < n {
|
|
||||||
if h.Less(j2, i) {
|
|
||||||
t.Errorf("heap invariant invalidated [%d] = %v > [%d] = %v", i, h[i], j1, h[j2])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.verify(t, j2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInit0(t *testing.T) {
|
|
||||||
h := new(myHeap[int])
|
|
||||||
for i := 20; i > 0; i-- {
|
|
||||||
h.Push(0) // all elements are the same
|
|
||||||
}
|
|
||||||
Init[int](h)
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 1; h.Len() > 0; i++ {
|
|
||||||
x := Pop[int](h)
|
|
||||||
h.verify(t, 0)
|
|
||||||
if x != 0 {
|
|
||||||
t.Errorf("%d.th pop got %d; want %d", i, x, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInit1(t *testing.T) {
|
|
||||||
h := new(myHeap[int])
|
|
||||||
for i := 20; i > 0; i-- {
|
|
||||||
h.Push(i) // all elements are different
|
|
||||||
}
|
|
||||||
Init[int](h)
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 1; h.Len() > 0; i++ {
|
|
||||||
x := Pop[int](h)
|
|
||||||
h.verify(t, 0)
|
|
||||||
if x != i {
|
|
||||||
t.Errorf("%d.th pop got %d; want %d", i, x, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
|
||||||
h := new(myHeap[int])
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 20; i > 10; i-- {
|
|
||||||
h.Push(i)
|
|
||||||
}
|
|
||||||
Init[int](h)
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 10; i > 0; i-- {
|
|
||||||
Push[int](h, i)
|
|
||||||
h.verify(t, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 1; h.Len() > 0; i++ {
|
|
||||||
x := Pop[int](h)
|
|
||||||
if i < 20 {
|
|
||||||
Push[int](h, 20+i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
if x != i {
|
|
||||||
t.Errorf("%d.th pop got %d; want %d", i, x, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemove0(t *testing.T) {
|
|
||||||
h := new(myHeap[int])
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
h.Push(i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for h.Len() > 0 {
|
|
||||||
i := h.Len() - 1
|
|
||||||
x := Remove[int](h, i)
|
|
||||||
if x != i {
|
|
||||||
t.Errorf("Remove(%d) got %d; want %d", i, x, i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemove1(t *testing.T) {
|
|
||||||
h := new(myHeap[int])
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
h.Push(i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 0; h.Len() > 0; i++ {
|
|
||||||
x := Remove[int](h, 0)
|
|
||||||
if x != i {
|
|
||||||
t.Errorf("Remove(0) got %d; want %d", x, i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemove2(t *testing.T) {
|
|
||||||
N := 10
|
|
||||||
|
|
||||||
h := new(myHeap[int])
|
|
||||||
for i := 0; i < N; i++ {
|
|
||||||
h.Push(i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
m := make(map[int]bool)
|
|
||||||
for h.Len() > 0 {
|
|
||||||
m[Remove[int](h, (h.Len()-1)/2)] = true
|
|
||||||
h.verify(t, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m) != N {
|
|
||||||
t.Errorf("len(m) = %d; want %d", len(m), N)
|
|
||||||
}
|
|
||||||
for i := 0; i < len(m); i++ {
|
|
||||||
if !m[i] {
|
|
||||||
t.Errorf("m[%d] doesn't exist", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDup(b *testing.B) {
|
|
||||||
const n = 10000
|
|
||||||
h := make(myHeap[int], 0, n)
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
for j := 0; j < n; j++ {
|
|
||||||
Push[int](&h, 0) // all elements are the same
|
|
||||||
}
|
|
||||||
for h.Len() > 0 {
|
|
||||||
Pop[int](&h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFix(t *testing.T) {
|
|
||||||
h := new(myHeap[int])
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 200; i > 0; i -= 10 {
|
|
||||||
Push[int](h, i)
|
|
||||||
}
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
if (*h)[0] != 10 {
|
|
||||||
t.Fatalf("Expected head to be 10, was %d", (*h)[0])
|
|
||||||
}
|
|
||||||
(*h)[0] = 210
|
|
||||||
Fix[int](h, 0)
|
|
||||||
h.verify(t, 0)
|
|
||||||
|
|
||||||
for i := 100; i > 0; i-- {
|
|
||||||
elem := rand.Intn(h.Len())
|
|
||||||
if i&1 == 0 {
|
|
||||||
(*h)[elem] *= 2
|
|
||||||
} else {
|
|
||||||
(*h)[elem] /= 2
|
|
||||||
}
|
|
||||||
Fix[int](h, elem)
|
|
||||||
h.verify(t, 0)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
"tailscale.com/cmd/testwrapper/flakytest"
|
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/net/netns"
|
"tailscale.com/net/netns"
|
||||||
|
@ -283,7 +282,6 @@ func TestConn(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoopbackLocalAPI(t *testing.T) {
|
func TestLoopbackLocalAPI(t *testing.T) {
|
||||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557")
|
|
||||||
tstest.ResourceCheck(t)
|
tstest.ResourceCheck(t)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -358,7 +356,6 @@ func TestLoopbackLocalAPI(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoopbackSOCKS5(t *testing.T) {
|
func TestLoopbackSOCKS5(t *testing.T) {
|
||||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8198")
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
699
tstest/clock.go
699
tstest/clock.go
|
@ -4,686 +4,57 @@
|
||||||
package tstest
|
package tstest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/tstime"
|
|
||||||
"tailscale.com/util/mak"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClockOpts is used to configure the initial settings for a Clock. Once the
|
|
||||||
// settings are configured as desired, call NewClock to get the resulting Clock.
|
|
||||||
type ClockOpts struct {
|
|
||||||
// Start is the starting time for the Clock. When FollowRealTime is false,
|
|
||||||
// Start is also the value that will be returned by the first call
|
|
||||||
// to Clock.Now.
|
|
||||||
Start time.Time
|
|
||||||
// Step is the amount of time the Clock will advance whenever Clock.Now is
|
|
||||||
// called. If set to zero, the Clock will only advance when Clock.Advance is
|
|
||||||
// called and/or if FollowRealTime is true.
|
|
||||||
//
|
|
||||||
// FollowRealTime and Step cannot be enabled at the same time.
|
|
||||||
Step time.Duration
|
|
||||||
|
|
||||||
// TimerChannelSize configures the maximum buffered ticks that are
|
|
||||||
// permitted in the channel of any Timer and Ticker created by this Clock.
|
|
||||||
// The special value 0 means to use the default of 1. The buffer may need to
|
|
||||||
// be increased if time is advanced by more than a single tick and proper
|
|
||||||
// functioning of the test requires that the ticks are not lost.
|
|
||||||
TimerChannelSize int
|
|
||||||
|
|
||||||
// FollowRealTime makes the simulated time increment along with real time.
|
|
||||||
// It is a compromise between determinism and the difficulty of explicitly
|
|
||||||
// managing the simulated time via Step or Clock.Advance. When
|
|
||||||
// FollowRealTime is set, calls to Now() and PeekNow() will add the
|
|
||||||
// elapsed real-world time to the simulated time.
|
|
||||||
//
|
|
||||||
// FollowRealTime and Step cannot be enabled at the same time.
|
|
||||||
FollowRealTime bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClock creates a Clock with the specified settings. To create a
|
|
||||||
// Clock with only the default settings, new(Clock) is equivalent, except that
|
|
||||||
// the start time will not be computed until one of the receivers is called.
|
|
||||||
func NewClock(co ClockOpts) *Clock {
|
|
||||||
if co.FollowRealTime && co.Step != 0 {
|
|
||||||
panic("only one of FollowRealTime and Step are allowed in NewClock")
|
|
||||||
}
|
|
||||||
|
|
||||||
return newClockInternal(co, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newClockInternal creates a Clock with the specified settings and allows
|
|
||||||
// specifying a non-standard realTimeClock.
|
|
||||||
func newClockInternal(co ClockOpts, rtClock tstime.Clock) *Clock {
|
|
||||||
if !co.FollowRealTime && rtClock != nil {
|
|
||||||
panic("rtClock can only be set with FollowRealTime enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if co.FollowRealTime && rtClock == nil {
|
|
||||||
rtClock = new(tstime.StdClock)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Clock{
|
|
||||||
start: co.Start,
|
|
||||||
realTimeClock: rtClock,
|
|
||||||
step: co.Step,
|
|
||||||
timerChannelSize: co.TimerChannelSize,
|
|
||||||
}
|
|
||||||
c.init() // init now to capture the current time when co.Start.IsZero()
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clock is a testing clock that advances every time its Now method is
|
// Clock is a testing clock that advances every time its Now method is
|
||||||
// called, beginning at its start time. If no start time is specified using
|
// called, beginning at Start.
|
||||||
// ClockBuilder, an arbitrary start time will be selected when the Clock is
|
//
|
||||||
// created and can be retrieved by calling Clock.Start().
|
// The zero value starts virtual time at an arbitrary value recorded
|
||||||
|
// in Start on the first call to Now, and time never advances.
|
||||||
type Clock struct {
|
type Clock struct {
|
||||||
// start is the first value returned by Now. It must not be modified after
|
// Start is the first value returned by Now.
|
||||||
// init is called.
|
Start time.Time
|
||||||
start time.Time
|
// Step is how much to advance with each Now call.
|
||||||
|
Step time.Duration
|
||||||
|
// Present is the time that the next Now call will receive.
|
||||||
|
Present time.Time
|
||||||
|
|
||||||
// realTimeClock, if not nil, indicates that the Clock shall move forward
|
sync.Mutex
|
||||||
// according to realTimeClock + the accumulated calls to Advance. This can
|
|
||||||
// make writing tests easier that require some control over the clock but do
|
|
||||||
// not need exact control over the clock. While step can also be used for
|
|
||||||
// this purpose, it is harder to control how quickly time moves using step.
|
|
||||||
realTimeClock tstime.Clock
|
|
||||||
|
|
||||||
initOnce sync.Once
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
// step is how much to advance with each Now call.
|
|
||||||
step time.Duration
|
|
||||||
// present is the last value returned by Now (and will be returned again by
|
|
||||||
// PeekNow).
|
|
||||||
present time.Time
|
|
||||||
// realTime is the time from realTimeClock corresponding to the current
|
|
||||||
// value of present.
|
|
||||||
realTime time.Time
|
|
||||||
// skipStep indicates that the next call to Now should not add step to
|
|
||||||
// present. This occurs after initialization and after Advance.
|
|
||||||
skipStep bool
|
|
||||||
// timerChannelSize is the buffer size to use for channels created by
|
|
||||||
// NewTimer and NewTicker.
|
|
||||||
timerChannelSize int
|
|
||||||
|
|
||||||
events eventManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Clock) init() {
|
|
||||||
c.initOnce.Do(func() {
|
|
||||||
if c.realTimeClock != nil {
|
|
||||||
c.realTime = c.realTimeClock.Now()
|
|
||||||
}
|
|
||||||
if c.start.IsZero() {
|
|
||||||
if c.realTime.IsZero() {
|
|
||||||
c.start = time.Now()
|
|
||||||
} else {
|
|
||||||
c.start = c.realTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.timerChannelSize == 0 {
|
|
||||||
c.timerChannelSize = 1
|
|
||||||
}
|
|
||||||
c.present = c.start
|
|
||||||
c.skipStep = true
|
|
||||||
c.events.AdvanceTo(c.present)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now returns the virtual clock's current time, and advances it
|
// Now returns the virtual clock's current time, and advances it
|
||||||
// according to its step configuration.
|
// according to its step configuration.
|
||||||
func (c *Clock) Now() time.Time {
|
func (c *Clock) Now() time.Time {
|
||||||
c.init()
|
c.Lock()
|
||||||
rt := c.maybeGetRealTime()
|
defer c.Unlock()
|
||||||
|
c.initLocked()
|
||||||
|
step := c.Step
|
||||||
|
ret := c.Present
|
||||||
|
c.Present = c.Present.Add(step)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
c.mu.Lock()
|
func (c *Clock) Advance(d time.Duration) {
|
||||||
defer c.mu.Unlock()
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
c.initLocked()
|
||||||
|
c.Present = c.Present.Add(d)
|
||||||
|
}
|
||||||
|
|
||||||
step := c.step
|
func (c *Clock) initLocked() {
|
||||||
if c.skipStep {
|
if c.Start.IsZero() {
|
||||||
step = 0
|
c.Start = time.Now()
|
||||||
c.skipStep = false
|
|
||||||
}
|
}
|
||||||
c.advanceLocked(rt, step)
|
if c.Present.Before(c.Start) {
|
||||||
|
c.Present = c.Start
|
||||||
return c.present
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Clock) maybeGetRealTime() time.Time {
|
|
||||||
if c.realTimeClock == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return c.realTimeClock.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Clock) advanceLocked(now time.Time, add time.Duration) {
|
|
||||||
if !now.IsZero() {
|
|
||||||
add += now.Sub(c.realTime)
|
|
||||||
c.realTime = now
|
|
||||||
}
|
|
||||||
if add == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.present = c.present.Add(add)
|
|
||||||
c.events.AdvanceTo(c.present)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekNow returns the last time reported by Now. If Now has never been called,
|
|
||||||
// PeekNow returns the same value as GetStart.
|
|
||||||
func (c *Clock) PeekNow() time.Time {
|
|
||||||
c.init()
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.present
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance moves simulated time forward or backwards by a relative amount. Any
|
|
||||||
// Timer or Ticker that is waiting will fire at the requested point in simulated
|
|
||||||
// time. Advance returns the new simulated time. If this Clock follows real time
|
|
||||||
// then the next call to Now will equal the return value of Advance + the
|
|
||||||
// elapsed time since calling Advance. Otherwise, the next call to Now will
|
|
||||||
// equal the return value of Advance, regardless of the current step.
|
|
||||||
func (c *Clock) Advance(d time.Duration) time.Time {
|
|
||||||
c.init()
|
|
||||||
rt := c.maybeGetRealTime()
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.skipStep = true
|
|
||||||
|
|
||||||
c.advanceLocked(rt, d)
|
|
||||||
return c.present
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdvanceTo moves simulated time to a new absolute value. Any Timer or Ticker
|
|
||||||
// that is waiting will fire at the requested point in simulated time. If this
|
|
||||||
// Clock follows real time then the next call to Now will equal t + the elapsed
|
|
||||||
// time since calling Advance. Otherwise, the next call to Now will equal t,
|
|
||||||
// regardless of the configured step.
|
|
||||||
func (c *Clock) AdvanceTo(t time.Time) {
|
|
||||||
c.init()
|
|
||||||
rt := c.maybeGetRealTime()
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.skipStep = true
|
|
||||||
c.realTime = rt
|
|
||||||
c.present = t
|
|
||||||
c.events.AdvanceTo(c.present)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStart returns the initial simulated time when this Clock was created.
|
|
||||||
func (c *Clock) GetStart() time.Time {
|
|
||||||
c.init()
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.start
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStep returns the amount that simulated time advances on every call to Now.
|
|
||||||
func (c *Clock) GetStep() time.Duration {
|
|
||||||
c.init()
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.step
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStep updates the amount that simulated time advances on every call to Now.
|
|
||||||
func (c *Clock) SetStep(d time.Duration) {
|
|
||||||
c.init()
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.step = d
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTimerChannelSize changes the channel size for any Timer or Ticker created
|
|
||||||
// in the future. It does not affect those that were already created.
|
|
||||||
func (c *Clock) SetTimerChannelSize(n int) {
|
|
||||||
c.init()
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.timerChannelSize = n
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTicker returns a Ticker that uses this Clock for accessing the current
|
|
||||||
// time.
|
|
||||||
func (c *Clock) NewTicker(d time.Duration) (tstime.TickerController, <-chan time.Time) {
|
|
||||||
c.init()
|
|
||||||
rt := c.maybeGetRealTime()
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.advanceLocked(rt, 0)
|
|
||||||
t := &Ticker{
|
|
||||||
nextTrigger: c.present.Add(d),
|
|
||||||
period: d,
|
|
||||||
em: &c.events,
|
|
||||||
}
|
|
||||||
t.init(c.timerChannelSize)
|
|
||||||
return t, t.C
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTimer returns a Timer that uses this Clock for accessing the current
|
|
||||||
// time.
|
|
||||||
func (c *Clock) NewTimer(d time.Duration) (tstime.TimerController, <-chan time.Time) {
|
|
||||||
c.init()
|
|
||||||
rt := c.maybeGetRealTime()
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.advanceLocked(rt, 0)
|
|
||||||
t := &Timer{
|
|
||||||
nextTrigger: c.present.Add(d),
|
|
||||||
em: &c.events,
|
|
||||||
}
|
|
||||||
t.init(c.timerChannelSize, nil)
|
|
||||||
return t, t.C
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterFunc returns a Timer that calls f when it fires, using this Clock for
|
|
||||||
// accessing the current time.
|
|
||||||
func (c *Clock) AfterFunc(d time.Duration, f func()) tstime.TimerController {
|
|
||||||
c.init()
|
|
||||||
rt := c.maybeGetRealTime()
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.advanceLocked(rt, 0)
|
|
||||||
t := &Timer{
|
|
||||||
nextTrigger: c.present.Add(d),
|
|
||||||
em: &c.events,
|
|
||||||
}
|
|
||||||
t.init(c.timerChannelSize, f)
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventHandler offers a common interface for Timer and Ticker events to avoid
|
|
||||||
// code duplication in eventManager.
|
|
||||||
type eventHandler interface {
|
|
||||||
// Fire signals the event. The provided time is written to the event's
|
|
||||||
// channel as the current time. The return value is the next time this event
|
|
||||||
// should fire, otherwise if it is zero then the event will be removed from
|
|
||||||
// the eventManager.
|
|
||||||
Fire(time.Time) time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// event tracks details about an upcoming Timer or Ticker firing.
|
|
||||||
type event struct {
|
|
||||||
position int // The current index in the heap, needed for heap.Fix and heap.Remove.
|
|
||||||
when time.Time // A cache of the next time the event triggers to avoid locking issues if we were to get it from eh.
|
|
||||||
eh eventHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventManager tracks pending events created by Timer and Ticker. eventManager
|
|
||||||
// implements heap.Interface for efficient lookups of the next event.
|
|
||||||
type eventManager struct {
|
|
||||||
// clock is a real time clock for scheduling events with. When clock is nil,
|
|
||||||
// events only fire when AdvanceTo is called by the simulated clock that
|
|
||||||
// this eventManager belongs to. When clock is not nil, events may fire when
|
|
||||||
// timer triggers.
|
|
||||||
clock tstime.Clock
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
now time.Time
|
|
||||||
heap []*event
|
|
||||||
reverseLookup map[eventHandler]*event
|
|
||||||
|
|
||||||
// timer is an AfterFunc that triggers at heap[0].when.Sub(now) relative to
|
|
||||||
// the time represented by clock. In other words, if clock is real world
|
|
||||||
// time, then if an event is scheduled 1 second into the future in the
|
|
||||||
// simulated time, then the event will trigger after 1 second of actual test
|
|
||||||
// execution time (unless the test advances simulated time, in which case
|
|
||||||
// the timer is updated accordingly). This makes tests easier to write in
|
|
||||||
// situations where the simulated time only needs to be partially
|
|
||||||
// controlled, and the test writer wishes for simulated time to pass with an
|
|
||||||
// offset but still synchronized with the real world.
|
|
||||||
//
|
|
||||||
// In the future, this could be extended to allow simulated time to run at a
|
|
||||||
// multiple of real world time.
|
|
||||||
timer tstime.TimerController
|
|
||||||
}
|
|
||||||
|
|
||||||
func (em *eventManager) handleTimer() {
|
|
||||||
rt := em.clock.Now()
|
|
||||||
em.AdvanceTo(rt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push implements heap.Interface.Push and must only be called by heap funcs
|
|
||||||
// with em.mu already held.
|
|
||||||
func (em *eventManager) Push(x any) {
|
|
||||||
e, ok := x.(*event)
|
|
||||||
if !ok {
|
|
||||||
panic("incorrect event type")
|
|
||||||
}
|
|
||||||
if e == nil {
|
|
||||||
panic("nil event")
|
|
||||||
}
|
|
||||||
|
|
||||||
mak.Set(&em.reverseLookup, e.eh, e)
|
|
||||||
e.position = len(em.heap)
|
|
||||||
em.heap = append(em.heap, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop implements heap.Interface.Pop and must only be called by heap funcs with
|
|
||||||
// em.mu already held.
|
|
||||||
func (em *eventManager) Pop() any {
|
|
||||||
e := em.heap[len(em.heap)-1]
|
|
||||||
em.heap = em.heap[:len(em.heap)-1]
|
|
||||||
delete(em.reverseLookup, e.eh)
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len implements sort.Interface.Len and must only be called by heap funcs with
|
|
||||||
// em.mu already held.
|
|
||||||
func (em *eventManager) Len() int {
|
|
||||||
return len(em.heap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Less implements sort.Interface.Less and must only be called by heap funcs
|
|
||||||
// with em.mu already held.
|
|
||||||
func (em *eventManager) Less(i, j int) bool {
|
|
||||||
return em.heap[i].when.Before(em.heap[j].when)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap implements sort.Interface.Swap and must only be called by heap funcs
|
|
||||||
// with em.mu already held.
|
|
||||||
func (em *eventManager) Swap(i, j int) {
|
|
||||||
em.heap[i], em.heap[j] = em.heap[j], em.heap[i]
|
|
||||||
em.heap[i].position = i
|
|
||||||
em.heap[j].position = j
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reschedule adds/updates/deletes an event in the heap, whichever
|
|
||||||
// operation is applicable (use a zero time to delete).
|
|
||||||
func (em *eventManager) Reschedule(eh eventHandler, t time.Time) {
|
|
||||||
em.mu.Lock()
|
|
||||||
defer em.mu.Unlock()
|
|
||||||
defer em.updateTimerLocked()
|
|
||||||
|
|
||||||
e, ok := em.reverseLookup[eh]
|
|
||||||
if !ok {
|
|
||||||
if t.IsZero() {
|
|
||||||
// eh is not scheduled and also not active, so do nothing.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// eh is not scheduled but is active, so add it.
|
|
||||||
heap.Push(em, &event{
|
|
||||||
when: t,
|
|
||||||
eh: eh,
|
|
||||||
})
|
|
||||||
em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.IsZero() {
|
|
||||||
// e is scheduled but not active, so remove it.
|
|
||||||
heap.Remove(em, e.position)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// e is scheduled and active, so update it.
|
|
||||||
e.when = t
|
|
||||||
heap.Fix(em, e.position)
|
|
||||||
em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdvanceTo updates the current time to tm and fires all events scheduled
|
|
||||||
// before or equal to tm. When an event fires, it may request rescheduling and
|
|
||||||
// the rescheduled events will be combined with the other existing events that
|
|
||||||
// are waiting, and will be run in the unified ordering. A poorly behaved event
|
|
||||||
// may theoretically prevent this from ever completing, but both Timer and
|
|
||||||
// Ticker require positive steps into the future.
|
|
||||||
func (em *eventManager) AdvanceTo(tm time.Time) {
|
|
||||||
em.mu.Lock()
|
|
||||||
defer em.mu.Unlock()
|
|
||||||
defer em.updateTimerLocked()
|
|
||||||
|
|
||||||
em.processEventsLocked(tm)
|
|
||||||
em.now = tm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now returns the cached current time. It is intended for use by a Timer or
|
|
||||||
// Ticker that needs to convert a relative time to an absolute time.
|
|
||||||
func (em *eventManager) Now() time.Time {
|
|
||||||
em.mu.Lock()
|
|
||||||
defer em.mu.Unlock()
|
|
||||||
return em.now
|
|
||||||
}
|
|
||||||
|
|
||||||
func (em *eventManager) processEventsLocked(tm time.Time) {
|
|
||||||
for len(em.heap) > 0 && !em.heap[0].when.After(tm) {
|
|
||||||
// Ideally some jitter would be added here but it's difficult to do so
|
|
||||||
// in a deterministic fashion.
|
|
||||||
em.now = em.heap[0].when
|
|
||||||
|
|
||||||
if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() {
|
|
||||||
em.heap[0].when = nextFire
|
|
||||||
heap.Fix(em, 0)
|
|
||||||
} else {
|
|
||||||
heap.Pop(em)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em *eventManager) updateTimerLocked() {
|
// Reset rewinds the virtual clock to its start time.
|
||||||
if em.clock == nil {
|
func (c *Clock) Reset() {
|
||||||
return
|
c.Lock()
|
||||||
}
|
defer c.Unlock()
|
||||||
if len(em.heap) == 0 {
|
c.Present = c.Start
|
||||||
if em.timer != nil {
|
|
||||||
em.timer.Stop()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
timeToEvent := em.heap[0].when.Sub(em.now)
|
|
||||||
if em.timer == nil {
|
|
||||||
em.timer = em.clock.AfterFunc(timeToEvent, em.handleTimer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
em.timer.Reset(timeToEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ticker is a time.Ticker lookalike for use in tests that need to control when
|
|
||||||
// events fire. Ticker could be made standalone in future but for now is
|
|
||||||
// expected to be paired with a Clock and created by Clock.NewTicker.
|
|
||||||
type Ticker struct {
|
|
||||||
C <-chan time.Time // The channel on which ticks are delivered.
|
|
||||||
|
|
||||||
// em is the eventManager to be notified when nextTrigger changes.
|
|
||||||
// eventManager has its own mutex, and the pointer is immutable, therefore
|
|
||||||
// em can be accessed without holding mu.
|
|
||||||
em *eventManager
|
|
||||||
|
|
||||||
c chan<- time.Time // The writer side of C.
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
// nextTrigger is the time of the ticker's next scheduled activation. When
|
|
||||||
// Fire activates the ticker, nextTrigger is the timestamp written to the
|
|
||||||
// channel.
|
|
||||||
nextTrigger time.Time
|
|
||||||
|
|
||||||
// period is the duration that is added to nextTrigger when the ticker
|
|
||||||
// fires.
|
|
||||||
period time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Ticker) init(channelSize int) {
|
|
||||||
if channelSize <= 0 {
|
|
||||||
panic("ticker channel size must be non-negative")
|
|
||||||
}
|
|
||||||
c := make(chan time.Time, channelSize)
|
|
||||||
t.c = c
|
|
||||||
t.C = c
|
|
||||||
t.em.Reschedule(t, t.nextTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire triggers the ticker. curTime is the timestamp to write to the channel.
|
|
||||||
// The next trigger time for the ticker is updated to the last computed trigger
|
|
||||||
// time + the ticker period (set at creation or using Reset). The next trigger
|
|
||||||
// time is computed this way to match standard time.Ticker behavior, which
|
|
||||||
// prevents accumulation of long term drift caused by delays in event execution.
|
|
||||||
func (t *Ticker) Fire(curTime time.Time) time.Time {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
if t.nextTrigger.IsZero() {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case t.c <- curTime:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
t.nextTrigger = t.nextTrigger.Add(t.period)
|
|
||||||
|
|
||||||
return t.nextTrigger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset adjusts the Ticker's period to d and reschedules the next fire time to
|
|
||||||
// the current simulated time + d.
|
|
||||||
func (t *Ticker) Reset(d time.Duration) {
|
|
||||||
if d <= 0 {
|
|
||||||
// The standard time.Ticker requires a positive period.
|
|
||||||
panic("non-positive period for Ticker.Reset")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := t.em.Now()
|
|
||||||
|
|
||||||
t.mu.Lock()
|
|
||||||
t.resetLocked(now.Add(d), d)
|
|
||||||
t.mu.Unlock()
|
|
||||||
|
|
||||||
t.em.Reschedule(t, t.nextTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetAbsolute adjusts the Ticker's period to d and reschedules the next fire
|
|
||||||
// time to nextTrigger.
|
|
||||||
func (t *Ticker) ResetAbsolute(nextTrigger time.Time, d time.Duration) {
|
|
||||||
if nextTrigger.IsZero() {
|
|
||||||
panic("zero nextTrigger time for ResetAbsolute")
|
|
||||||
}
|
|
||||||
if d <= 0 {
|
|
||||||
panic("non-positive period for ResetAbsolute")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.mu.Lock()
|
|
||||||
t.resetLocked(nextTrigger, d)
|
|
||||||
t.mu.Unlock()
|
|
||||||
|
|
||||||
t.em.Reschedule(t, t.nextTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Ticker) resetLocked(nextTrigger time.Time, d time.Duration) {
|
|
||||||
t.nextTrigger = nextTrigger
|
|
||||||
t.period = d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop deactivates the Ticker.
|
|
||||||
func (t *Ticker) Stop() {
|
|
||||||
t.mu.Lock()
|
|
||||||
t.nextTrigger = time.Time{}
|
|
||||||
t.mu.Unlock()
|
|
||||||
|
|
||||||
t.em.Reschedule(t, t.nextTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer is a time.Timer lookalike for use in tests that need to control when
|
|
||||||
// events fire. Timer could be made standalone in future but for now must be
|
|
||||||
// paired with a Clock and created by Clock.NewTimer.
|
|
||||||
type Timer struct {
|
|
||||||
C <-chan time.Time // The channel on which ticks are delivered.
|
|
||||||
|
|
||||||
// em is the eventManager to be notified when nextTrigger changes.
|
|
||||||
// eventManager has its own mutex, and the pointer is immutable, therefore
|
|
||||||
// em can be accessed without holding mu.
|
|
||||||
em *eventManager
|
|
||||||
|
|
||||||
f func(time.Time) // The function to call when the timer expires.
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
// nextTrigger is the time of the ticker's next scheduled activation. When
|
|
||||||
// Fire activates the ticker, nextTrigger is the timestamp written to the
|
|
||||||
// channel.
|
|
||||||
nextTrigger time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Timer) init(channelSize int, afterFunc func()) {
|
|
||||||
if channelSize <= 0 {
|
|
||||||
panic("ticker channel size must be non-negative")
|
|
||||||
}
|
|
||||||
c := make(chan time.Time, channelSize)
|
|
||||||
t.C = c
|
|
||||||
if afterFunc == nil {
|
|
||||||
t.f = func(curTime time.Time) {
|
|
||||||
select {
|
|
||||||
case c <- curTime:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.f = func(_ time.Time) { afterFunc() }
|
|
||||||
}
|
|
||||||
t.em.Reschedule(t, t.nextTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire triggers the ticker. curTime is the timestamp to write to the channel.
|
|
||||||
// The next trigger time for the ticker is updated to the last computed trigger
|
|
||||||
// time + the ticker period (set at creation or using Reset). The next trigger
|
|
||||||
// time is computed this way to match standard time.Ticker behavior, which
|
|
||||||
// prevents accumulation of long term drift caused by delays in event execution.
|
|
||||||
func (t *Timer) Fire(curTime time.Time) time.Time {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
if t.nextTrigger.IsZero() {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
t.nextTrigger = time.Time{}
|
|
||||||
t.f(curTime)
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset reschedules the next fire time to the current simulated time + d.
|
|
||||||
// Reset reports whether the timer was still active before the reset.
|
|
||||||
func (t *Timer) Reset(d time.Duration) bool {
|
|
||||||
if d <= 0 {
|
|
||||||
// The standard time.Timer requires a positive delay.
|
|
||||||
panic("non-positive delay for Timer.Reset")
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.reset(t.em.Now().Add(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetAbsolute reschedules the next fire time to nextTrigger.
|
|
||||||
// ResetAbsolute reports whether the timer was still active before the reset.
|
|
||||||
func (t *Timer) ResetAbsolute(nextTrigger time.Time) bool {
|
|
||||||
if nextTrigger.IsZero() {
|
|
||||||
panic("zero nextTrigger time for ResetAbsolute")
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.reset(nextTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop deactivates the Timer. Stop reports whether the timer was active before
|
|
||||||
// stopping.
|
|
||||||
func (t *Timer) Stop() bool {
|
|
||||||
return t.reset(time.Time{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Timer) reset(nextTrigger time.Time) bool {
|
|
||||||
t.mu.Lock()
|
|
||||||
wasActive := !t.nextTrigger.IsZero()
|
|
||||||
t.nextTrigger = nextTrigger
|
|
||||||
t.mu.Unlock()
|
|
||||||
|
|
||||||
t.em.Reschedule(t, t.nextTrigger)
|
|
||||||
return wasActive
|
|
||||||
}
|
}
|
||||||
|
|
2439
tstest/clock_test.go
2439
tstest/clock_test.go
File diff suppressed because it is too large
Load Diff
|
@ -104,8 +104,7 @@ func (t Time) WallTime() time.Time {
|
||||||
|
|
||||||
// MarshalJSON formats t for JSON as if it were a time.Time.
|
// MarshalJSON formats t for JSON as if it were a time.Time.
|
||||||
// We format Time this way for backwards-compatibility.
|
// We format Time this way for backwards-compatibility.
|
||||||
// Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged
|
// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged.
|
||||||
// across different invocations of the Go process. This is best-effort only.
|
|
||||||
// Since t is a monotonic time, it can vary from the actual wall clock by arbitrary amounts.
|
// Since t is a monotonic time, it can vary from the actual wall clock by arbitrary amounts.
|
||||||
// Even in the best of circumstances, it may vary by a few milliseconds.
|
// Even in the best of circumstances, it may vary by a few milliseconds.
|
||||||
func (t Time) MarshalJSON() ([]byte, error) {
|
func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -114,8 +113,7 @@ func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON sets t according to data.
|
// UnmarshalJSON sets t according to data.
|
||||||
// Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged
|
// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged.
|
||||||
// across different invocations of the Go process. This is best-effort only.
|
|
||||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||||
var tt time.Time
|
var tt time.Time
|
||||||
err := tt.UnmarshalJSON(data)
|
err := tt.UnmarshalJSON(data)
|
||||||
|
@ -126,6 +124,6 @@ func (t *Time) UnmarshalJSON(data []byte) error {
|
||||||
*t = 0
|
*t = 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
*t = baseMono.Add(tt.Sub(baseWall))
|
*t = Now().Add(-time.Since(tt))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,21 +33,6 @@ func TestUnmarshalZero(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONRoundtrip(t *testing.T) {
|
|
||||||
want := Now()
|
|
||||||
b, err := want.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("MarshalJSON error: %v", err)
|
|
||||||
}
|
|
||||||
var got Time
|
|
||||||
if err := got.UnmarshalJSON(b); err != nil {
|
|
||||||
t.Errorf("UnmarshalJSON error: %v", err)
|
|
||||||
}
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("got %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkMonoNow(b *testing.B) {
|
func BenchmarkMonoNow(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
|
|
|
@ -59,79 +59,3 @@ func Sleep(ctx context.Context, d time.Duration) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clock offers a subset of the functionality from the std/time package.
|
|
||||||
// Normally, applications will use the StdClock implementation that calls the
|
|
||||||
// appropriate std/time exported funcs. The advantage of using Clock is that
|
|
||||||
// tests can substitute a different implementation, allowing the test to control
|
|
||||||
// time precisely, something required for certain types of tests to be possible
|
|
||||||
// at all, speeds up execution by not needing to sleep, and can dramatically
|
|
||||||
// reduce the risk of flakes due to tests executing too slowly or quickly.
|
|
||||||
type Clock interface {
|
|
||||||
// Now returns the current time, as in time.Now.
|
|
||||||
Now() time.Time
|
|
||||||
// NewTimer returns a timer whose notion of the current time is controlled
|
|
||||||
// by this Clock. It follows the semantics of time.NewTimer as closely as
|
|
||||||
// possible but is adapted to return an interface, so the channel needs to
|
|
||||||
// be returned as well.
|
|
||||||
NewTimer(d time.Duration) (TimerController, <-chan time.Time)
|
|
||||||
// NewTicker returns a ticker whose notion of the current time is controlled
|
|
||||||
// by this Clock. It follows the semantics of time.NewTicker as closely as
|
|
||||||
// possible but is adapted to return an interface, so the channel needs to
|
|
||||||
// be returned as well.
|
|
||||||
NewTicker(d time.Duration) (TickerController, <-chan time.Time)
|
|
||||||
// AfterFunc returns a ticker whose notion of the current time is controlled
|
|
||||||
// by this Clock. When the ticker expires, it will call the provided func.
|
|
||||||
// It follows the semantics of time.AfterFunc.
|
|
||||||
AfterFunc(d time.Duration, f func()) TimerController
|
|
||||||
}
|
|
||||||
|
|
||||||
// TickerController offers the receivers of a time.Ticker to ensure
|
|
||||||
// compatibility with standard timers, but allows for the option of substituting
|
|
||||||
// a standard timer with something else for testing purposes.
|
|
||||||
type TickerController interface {
|
|
||||||
// Reset follows the same semantics as with time.Ticker.Reset.
|
|
||||||
Reset(d time.Duration)
|
|
||||||
// Stop follows the same semantics as with time.Ticker.Stop.
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimerController offers the receivers of a time.Timer to ensure
|
|
||||||
// compatibility with standard timers, but allows for the option of substituting
|
|
||||||
// a standard timer with something else for testing purposes.
|
|
||||||
type TimerController interface {
|
|
||||||
// Reset follows the same semantics as with time.Timer.Reset.
|
|
||||||
Reset(d time.Duration) bool
|
|
||||||
// Stop follows the same semantics as with time.Timer.Stop.
|
|
||||||
Stop() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// StdClock is a simple implementation of Clock using the relevant funcs in the
|
|
||||||
// std/time package.
|
|
||||||
type StdClock struct{}
|
|
||||||
|
|
||||||
// Now calls time.Now.
|
|
||||||
func (StdClock) Now() time.Time {
|
|
||||||
return time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTimer calls time.NewTimer. As an interface does not allow for struct
|
|
||||||
// members and other packages cannot add receivers to another package, the
|
|
||||||
// channel is also returned because it would be otherwise inaccessible.
|
|
||||||
func (StdClock) NewTimer(d time.Duration) (TimerController, <-chan time.Time) {
|
|
||||||
t := time.NewTimer(d)
|
|
||||||
return t, t.C
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTicker calls time.NewTicker. As an interface does not allow for struct
|
|
||||||
// members and other packages cannot add receivers to another package, the
|
|
||||||
// channel is also returned because it would be otherwise inaccessible.
|
|
||||||
func (StdClock) NewTicker(d time.Duration) (TickerController, <-chan time.Time) {
|
|
||||||
t := time.NewTicker(d)
|
|
||||||
return t, t.C
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterFunc calls time.AfterFunc.
|
|
||||||
func (StdClock) AfterFunc(d time.Duration, f func()) TimerController {
|
|
||||||
return time.AfterFunc(d, f)
|
|
||||||
}
|
|
||||||
|
|
|
@ -65,7 +65,10 @@ func TestStdHandler(t *testing.T) {
|
||||||
testErr = errors.New("test error")
|
testErr = errors.New("test error")
|
||||||
bgCtx = context.Background()
|
bgCtx = context.Background()
|
||||||
// canceledCtx, cancel = context.WithCancel(bgCtx)
|
// canceledCtx, cancel = context.WithCancel(bgCtx)
|
||||||
startTime = time.Unix(1687870000, 1234)
|
clock = tstest.Clock{
|
||||||
|
Start: time.Now(),
|
||||||
|
Step: time.Second,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
// cancel()
|
// cancel()
|
||||||
|
|
||||||
|
@ -83,7 +86,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/"),
|
r: req(bgCtx, "http://example.com/"),
|
||||||
wantCode: 200,
|
wantCode: 200,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
TLS: false,
|
TLS: false,
|
||||||
|
@ -100,7 +103,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 404,
|
wantCode: 404,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -116,7 +119,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 404,
|
wantCode: 404,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -133,7 +136,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 404,
|
wantCode: 404,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -150,7 +153,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 500,
|
wantCode: 500,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -167,7 +170,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 500,
|
wantCode: 500,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -184,7 +187,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 500,
|
wantCode: 500,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -201,7 +204,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 200,
|
wantCode: 200,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -218,7 +221,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 200,
|
wantCode: 200,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -235,7 +238,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 200,
|
wantCode: 200,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
|
@ -257,7 +260,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
r: req(bgCtx, "http://example.com/foo"),
|
r: req(bgCtx, "http://example.com/foo"),
|
||||||
wantCode: 200,
|
wantCode: 200,
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
|
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
|
@ -276,7 +279,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
http.Error(w, e.Msg, 200)
|
http.Error(w, e.Msg, 200)
|
||||||
},
|
},
|
||||||
wantLog: AccessLogRecord{
|
wantLog: AccessLogRecord{
|
||||||
When: startTime,
|
When: clock.Start,
|
||||||
Seconds: 1.0,
|
Seconds: 1.0,
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
TLS: false,
|
TLS: false,
|
||||||
|
@ -299,10 +302,7 @@ func TestStdHandler(t *testing.T) {
|
||||||
t.Logf(fmt, args...)
|
t.Logf(fmt, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
clock := tstest.NewClock(tstest.ClockOpts{
|
clock.Reset()
|
||||||
Start: startTime,
|
|
||||||
Step: time.Second,
|
|
||||||
})
|
|
||||||
|
|
||||||
rec := noopHijacker{httptest.NewRecorder(), false}
|
rec := noopHijacker{httptest.NewRecorder(), false}
|
||||||
h := StdHandler(test.rh, HandlerOptions{Logf: logf, Now: clock.Now, OnError: test.errHandler})
|
h := StdHandler(test.rh, HandlerOptions{Logf: logf, Now: clock.Now, OnError: test.errHandler})
|
||||||
|
|
|
@ -189,7 +189,7 @@ func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
|
||||||
// IntMap uses expvar.Map on the inside, which presorts
|
// IntMap uses expvar.Map on the inside, which presorts
|
||||||
// keys. The output ordering is deterministic.
|
// keys. The output ordering is deterministic.
|
||||||
v.Do(func(kv expvar.KeyValue) {
|
v.Do(func(kv expvar.KeyValue) {
|
||||||
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, cmpx.Or(v.Label, "label"), kv.Key, kv.Value)
|
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, v.Label, kv.Key, kv.Value)
|
||||||
})
|
})
|
||||||
case *expvar.Map:
|
case *expvar.Map:
|
||||||
if label != "" && typ != "" {
|
if label != "" && typ != "" {
|
||||||
|
|
|
@ -165,16 +165,6 @@ func TestVarzHandler(t *testing.T) {
|
||||||
})(),
|
})(),
|
||||||
"control_save_config{reason=\"fun\"} 1\ncontrol_save_config{reason=\"new\"} 1\ncontrol_save_config{reason=\"updated\"} 1\n",
|
"control_save_config{reason=\"fun\"} 1\ncontrol_save_config{reason=\"new\"} 1\ncontrol_save_config{reason=\"updated\"} 1\n",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"metrics_label_map_unlabeled",
|
|
||||||
"foo",
|
|
||||||
(func() *metrics.LabelMap {
|
|
||||||
m := &metrics.LabelMap{Label: ""}
|
|
||||||
m.Add("a", 1)
|
|
||||||
return m
|
|
||||||
})(),
|
|
||||||
"foo{label=\"a\"} 1\n",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"expvar_label_map",
|
"expvar_label_map",
|
||||||
"counter_labelmap_keyname_m",
|
"counter_labelmap_keyname_m",
|
||||||
|
|
|
@ -10,12 +10,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error {
|
func unmarshalJSON[T any](b []byte, x *[]T) error {
|
||||||
if *x != nil {
|
if *x != nil {
|
||||||
return errors.New("already initialized")
|
return errors.New("already initialized")
|
||||||
}
|
}
|
||||||
|
@ -65,7 +64,7 @@ type SliceView[T ViewCloner[T, V], V StructView[T]] struct {
|
||||||
func (v SliceView[T, V]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
func (v SliceView[T, V]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
func (v *SliceView[T, V]) UnmarshalJSON(b []byte) error { return unmarshalSliceFromJSON(b, &v.ж) }
|
func (v *SliceView[T, V]) UnmarshalJSON(b []byte) error { return unmarshalJSON(b, &v.ж) }
|
||||||
|
|
||||||
// IsNil reports whether the underlying slice is nil.
|
// IsNil reports whether the underlying slice is nil.
|
||||||
func (v SliceView[T, V]) IsNil() bool { return v.ж == nil }
|
func (v SliceView[T, V]) IsNil() bool { return v.ж == nil }
|
||||||
|
@ -120,7 +119,7 @@ func (v Slice[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
func (v *Slice[T]) UnmarshalJSON(b []byte) error {
|
func (v *Slice[T]) UnmarshalJSON(b []byte) error {
|
||||||
return unmarshalSliceFromJSON(b, &v.ж)
|
return unmarshalJSON(b, &v.ж)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsNil reports whether the underlying slice is nil.
|
// IsNil reports whether the underlying slice is nil.
|
||||||
|
@ -333,30 +332,6 @@ func (m Map[K, V]) GetOk(k K) (V, bool) {
|
||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler.
|
|
||||||
func (m Map[K, V]) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(m.ж)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
|
||||||
// It should only be called on an uninitialized Map.
|
|
||||||
func (m *Map[K, V]) UnmarshalJSON(b []byte) error {
|
|
||||||
if m.ж != nil {
|
|
||||||
return errors.New("already initialized")
|
|
||||||
}
|
|
||||||
return json.Unmarshal(b, &m.ж)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsMap returns a shallow-clone of the underlying map.
|
|
||||||
// If V is a pointer type, it is the caller's responsibility to make sure
|
|
||||||
// the values are immutable.
|
|
||||||
func (m *Map[K, V]) AsMap() map[K]V {
|
|
||||||
if m == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return maps.Clone(m.ж)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MapRangeFn is the func called from a Map.Range call.
|
// MapRangeFn is the func called from a Map.Range call.
|
||||||
// Implementations should return false to stop range.
|
// Implementations should return false to stop range.
|
||||||
type MapRangeFn[K comparable, V any] func(k K, v V) (cont bool)
|
type MapRangeFn[K comparable, V any] func(k K, v V) (cont bool)
|
||||||
|
|
|
@ -581,8 +581,8 @@ func TestGetTypeHasher(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "tailcfg.Node",
|
name: "tailcfg.Node",
|
||||||
val: &tailcfg.Node{},
|
val: &tailcfg.Node{},
|
||||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
out32: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
out32: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// TODO(#8502): add support for more architectures
|
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||||
//go:build linux && (arm64 || amd64)
|
|
||||||
|
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// TODO(#8502): add support for more architectures
|
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||||
//go:build linux && (arm64 || amd64)
|
|
||||||
|
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// NOTE: linux_{arm64, x86} are the only two currently supported archs due to missing
|
// NOTE: linux_{386,loong64,arm,armbe} are currently unsupported due to missing
|
||||||
// support in upstream dependencies.
|
// support in upstream dependencies.
|
||||||
|
|
||||||
// TODO(#8502): add support for more architectures
|
//go:build !linux || (linux && (386 || loong64 || arm || armbe))
|
||||||
//go:build !linux || (linux && !(arm64 || amd64))
|
|
||||||
|
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// TODO(#8502): add support for more architectures
|
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||||
//go:build linux && (arm64 || amd64)
|
|
||||||
|
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// TODO(#8502): add support for more architectures
|
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||||
//go:build linux && (arm64 || amd64)
|
|
||||||
|
|
||||||
package linuxfw
|
package linuxfw
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ func IsAppleTV() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return isAppleTV.Get(func() bool {
|
return isAppleTV.Get(func() bool {
|
||||||
return strings.EqualFold(os.Getenv("XPC_SERVICE_NAME"), "io.tailscale.ipn.ios.network-extension-tvos")
|
return strings.EqualFold(os.Getenv("XPC_SERVICE_NAME"), "io.tailscale.ipn.tvos.network-extension")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@ import (
|
||||||
|
|
||||||
"github.com/tailscale/wireguard-go/conn"
|
"github.com/tailscale/wireguard-go/conn"
|
||||||
"go4.org/mem"
|
"go4.org/mem"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
|
@ -4410,12 +4409,16 @@ func (de *endpoint) addrForWireGuardSendLocked(now mono.Time) (udpAddr netip.Add
|
||||||
return udpAddr, false
|
return udpAddr, false
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := maps.Keys(de.endpointState)
|
candidates := make([]netip.AddrPort, 0, len(de.endpointState))
|
||||||
if len(candidates) == 0 {
|
for ipp := range de.endpointState {
|
||||||
de.c.logf("magicsock: addrForSendWireguardLocked: [unexpected] no candidates available for endpoint")
|
if ipp.Addr().Is4() && de.c.noV4.Load() {
|
||||||
return udpAddr, false
|
continue
|
||||||
|
}
|
||||||
|
if ipp.Addr().Is6() && de.c.noV6.Load() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidates = append(candidates, ipp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Randomly select an address to use until we retrieve latency information
|
// Randomly select an address to use until we retrieve latency information
|
||||||
// and give it a short trustBestAddrUntil time so we avoid flapping between
|
// and give it a short trustBestAddrUntil time so we avoid flapping between
|
||||||
// addresses while waiting on latency information to be populated.
|
// addresses while waiting on latency information to be populated.
|
||||||
|
|
|
@ -2809,6 +2809,36 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: netip.MustParseAddrPort("[2345:0425:2CA1:0000:0000:0567:5673:23b5]:222"),
|
want: netip.MustParseAddrPort("[2345:0425:2CA1:0000:0000:0567:5673:23b5]:222"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "choose IPv4 when IPv6 is not useable",
|
||||||
|
sendWGPing: false,
|
||||||
|
noV6: true,
|
||||||
|
ep: []endpointDetails{
|
||||||
|
{
|
||||||
|
addrPort: netip.MustParseAddrPort("1.1.1.1:111"),
|
||||||
|
latency: 100 * time.Millisecond,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addrPort: netip.MustParseAddrPort("[1::1]:567"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: netip.MustParseAddrPort("1.1.1.1:111"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "choose IPv6 when IPv4 is not useable",
|
||||||
|
sendWGPing: false,
|
||||||
|
noV4: true,
|
||||||
|
ep: []endpointDetails{
|
||||||
|
{
|
||||||
|
addrPort: netip.MustParseAddrPort("1.1.1.1:111"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addrPort: netip.MustParseAddrPort("[1::1]:567"),
|
||||||
|
latency: 100 * time.Millisecond,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: netip.MustParseAddrPort("[1::1]:567"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "choose IPv6 address when latency is the same for v4 and v6",
|
name: "choose IPv6 address when latency is the same for v4 and v6",
|
||||||
sendWGPing: true,
|
sendWGPing: true,
|
||||||
|
@ -2835,6 +2865,8 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
|
||||||
noV6: atomic.Bool{},
|
noV6: atomic.Bool{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
endpoint.c.noV4.Store(test.noV4)
|
||||||
|
endpoint.c.noV6.Store(test.noV6)
|
||||||
|
|
||||||
for _, epd := range test.ep {
|
for _, epd := range test.ep {
|
||||||
endpoint.endpointState[epd.addrPort] = &endpointState{}
|
endpoint.endpointState[epd.addrPort] = &endpointState{}
|
||||||
|
|
|
@ -96,6 +96,9 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
|
||||||
DiscoKey: peer.DiscoKey,
|
DiscoKey: peer.DiscoKey,
|
||||||
})
|
})
|
||||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||||
|
if peer.KeepAlive {
|
||||||
|
cpeer.PersistentKeepalive = 25 // seconds
|
||||||
|
}
|
||||||
|
|
||||||
didExitNodeWarn := false
|
didExitNodeWarn := false
|
||||||
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer
|
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer
|
||||||
|
|
|
@ -544,4 +544,3 @@ shrimp
|
||||||
prawn
|
prawn
|
||||||
lobster
|
lobster
|
||||||
chipmunk
|
chipmunk
|
||||||
tails
|
|
||||||
|
|
Loading…
Reference in New Issue