Compare commits
3 Commits
main
...
thisispark
Author | SHA1 | Date |
---|---|---|
![]() |
1f49e87cad | |
![]() |
d7df0d56be | |
![]() |
5c6724cb42 |
|
@ -946,21 +946,6 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
|
|||
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.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
|
|||
|
||||
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
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
|
||||
|
|
|
@ -12,16 +12,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
|
@ -30,7 +23,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
|
||||
|
@ -42,9 +34,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
|
||||
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
|
||||
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter
|
||||
|
@ -77,20 +66,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
L gvisor.dev/gvisor/pkg/abi from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/abi/linux from tailscale.com/util/linuxfw
|
||||
L gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L 💣 gvisor.dev/gvisor/pkg/hostarch from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
L gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context
|
||||
L gvisor.dev/gvisor/pkg/marshal from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L 💣 gvisor.dev/gvisor/pkg/marshal/primitive from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
L 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
L gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context
|
||||
nhooyr.io/websocket from tailscale.com/cmd/derper+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
|
@ -118,7 +93,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/cmd/derper
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
|
@ -156,9 +130,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L 💣 tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/multierr from tailscale.com/health+
|
||||
tailscale.com/util/multierr from tailscale.com/health
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
|
||||
|
@ -182,7 +155,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/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
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+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
|
|
|
@ -465,16 +465,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
|
||||
// Provide a better help message for when someone clicks through the signing flow
|
||||
// on the wrong device.
|
||||
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
|
||||
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
}
|
||||
return err
|
||||
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
|
||||
}
|
||||
|
||||
var nlDisableCmd = &ffcli.Command{
|
||||
|
|
|
@ -10,15 +10,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from tailscale.com/util/quarantine+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
|
@ -30,7 +23,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||
|
@ -44,30 +36,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
|
||||
L gvisor.dev/gvisor/pkg/abi from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/abi/linux from tailscale.com/util/linuxfw
|
||||
L gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L 💣 gvisor.dev/gvisor/pkg/hostarch from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
L gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context
|
||||
L gvisor.dev/gvisor/pkg/marshal from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L 💣 gvisor.dev/gvisor/pkg/marshal/primitive from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
L 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
L gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context
|
||||
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
|
@ -109,7 +84,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
|
@ -146,7 +120,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
L 💣 tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
|
||||
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli
|
||||
|
@ -173,7 +146,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/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
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/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
|
|
|
@ -75,7 +75,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
|
||||
|
@ -86,12 +86,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
|
@ -115,7 +109,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
|
@ -160,18 +153,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
|
||||
L gvisor.dev/gvisor/pkg/abi from gvisor.dev/gvisor/pkg/abi/linux
|
||||
L 💣 gvisor.dev/gvisor/pkg/abi/linux from tailscale.com/util/linuxfw
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2
|
||||
💣 gvisor.dev/gvisor/pkg/bufferv2 from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
L 💣 gvisor.dev/gvisor/pkg/hostarch from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
L gvisor.dev/gvisor/pkg/marshal from gvisor.dev/gvisor/pkg/abi/linux+
|
||||
L 💣 gvisor.dev/gvisor/pkg/marshal/primitive from gvisor.dev/gvisor/pkg/abi/linux
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/bufferv2+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
|
@ -276,7 +264,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
|
@ -330,7 +317,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L 💣 tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/logpolicy
|
||||
|
@ -379,7 +365,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+
|
||||
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/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/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
|
|
|
@ -28,9 +28,6 @@ func TestIssueFormat(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestFlakeRun is a test that fails when run in the testwrapper
|
||||
// for the first time, but succeeds on the second run.
|
||||
// It's used to test whether the testwrapper retries flaky tests.
|
||||
func TestFlakeRun(t *testing.T) {
|
||||
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
|
||||
e := os.Getenv(FlakeAttemptEnv)
|
||||
|
@ -38,6 +35,6 @@ func TestFlakeRun(t *testing.T) {
|
|||
t.Skip("not running in testwrapper")
|
||||
}
|
||||
if e == "1" {
|
||||
t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
|
||||
t.Fatal("failing on purpose")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,6 @@ type testAttempt struct {
|
|||
outcome string // "pass", "fail", "skip"
|
||||
logs bytes.Buffer
|
||||
isMarkedFlaky bool // set if the test is marked as flaky
|
||||
|
||||
pkgFinished bool
|
||||
}
|
||||
|
||||
type testName struct {
|
||||
|
@ -61,12 +59,7 @@ type goTestOutput struct {
|
|||
|
||||
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||
|
||||
// runTests runs the tests in pt and sends the results on ch. It sends a
|
||||
// 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)
|
||||
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string) []*testAttempt {
|
||||
args := []string{"test", "-json", pt.pattern}
|
||||
args = append(args, otherArgs...)
|
||||
if len(pt.tests) > 0 {
|
||||
|
@ -98,6 +91,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
|||
|
||||
jd := json.NewDecoder(r)
|
||||
resultMap := make(map[testName]*testAttempt)
|
||||
var out []*testAttempt
|
||||
for {
|
||||
var goOutput goTestOutput
|
||||
if err := jd.Decode(&goOutput); err != nil {
|
||||
|
@ -107,16 +101,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
|||
panic(err)
|
||||
}
|
||||
if goOutput.Test == "" {
|
||||
switch goOutput.Action {
|
||||
case "fail", "pass", "skip":
|
||||
ch <- &testAttempt{
|
||||
name: testName{
|
||||
pkg: goOutput.Package,
|
||||
},
|
||||
outcome: goOutput.Action,
|
||||
pkgFinished: true,
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
name := testName{
|
||||
|
@ -139,7 +123,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
|||
}
|
||||
case "skip", "pass", "fail":
|
||||
resultMap[name].outcome = goOutput.Action
|
||||
ch <- resultMap[name]
|
||||
out = append(out, resultMap[name])
|
||||
case "output":
|
||||
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
|
||||
resultMap[name].isMarkedFlaky = true
|
||||
|
@ -149,6 +133,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
|||
}
|
||||
}
|
||||
<-done
|
||||
return out
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -189,90 +174,58 @@ func main() {
|
|||
}
|
||||
pattern, otherArgs := args[0], args[1:]
|
||||
|
||||
type nextRun struct {
|
||||
tests []*packageTests
|
||||
attempt int
|
||||
toRun := []*packageTests{ // packages still to test
|
||||
{pattern: pattern},
|
||||
}
|
||||
|
||||
toRun := []*nextRun{
|
||||
{
|
||||
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)
|
||||
}
|
||||
pkgAttempts := make(map[string]int) // tracks how many times we've tried a package
|
||||
|
||||
attempt := 0
|
||||
for len(toRun) > 0 {
|
||||
var thisRun *nextRun
|
||||
thisRun, toRun = toRun[0], toRun[1:]
|
||||
attempt++
|
||||
var pt *packageTests
|
||||
pt, toRun = toRun[0], toRun[1:]
|
||||
|
||||
if thisRun.attempt >= maxAttempts {
|
||||
fmt.Println("max attempts reached")
|
||||
os.Exit(1)
|
||||
}
|
||||
if thisRun.attempt > 1 {
|
||||
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
|
||||
}
|
||||
toRetry := make(map[string][]string) // pkg -> tests to retry
|
||||
|
||||
failed := false
|
||||
toRetry := make(map[string][]string) // pkg -> tests to retry
|
||||
for _, pt := range thisRun.tests {
|
||||
ch := make(chan *testAttempt)
|
||||
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
|
||||
for tr := range ch {
|
||||
if tr.pkgFinished {
|
||||
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
|
||||
continue
|
||||
}
|
||||
if *v || tr.outcome == "fail" {
|
||||
io.Copy(os.Stdout, &tr.logs)
|
||||
}
|
||||
if tr.outcome != "fail" {
|
||||
continue
|
||||
}
|
||||
if tr.isMarkedFlaky {
|
||||
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
for _, tr := range runTests(ctx, attempt, pt, otherArgs) {
|
||||
if *v || tr.outcome == "fail" {
|
||||
io.Copy(os.Stderr, &tr.logs)
|
||||
}
|
||||
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 {
|
||||
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(toRetry) == 0 {
|
||||
continue
|
||||
}
|
||||
pkgs := maps.Keys(toRetry)
|
||||
sort.Strings(pkgs)
|
||||
nextRun := &nextRun{
|
||||
attempt: thisRun.attempt + 1,
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
tests := toRetry[pkg]
|
||||
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,
|
||||
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")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
nc.mu.Lock()
|
||||
if last := nc.last; last != nil && last.canTakeNewRequest() {
|
||||
|
@ -314,35 +295,11 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
|
|||
}
|
||||
nc.mu.Unlock()
|
||||
|
||||
for {
|
||||
// We singeflight the dial to avoid making multiple connections, however
|
||||
// that means that we can't simply cancel the dial if the context is
|
||||
// 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()
|
||||
conn, err, _ := nc.sfDial.Do(struct{}{}, nc.dial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
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
|
||||
// if not cached.
|
||||
func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
||||
func (nc *NoiseClient) dial() (*noiseConn, error) {
|
||||
nc.mu.Lock()
|
||||
connID := nc.nextID
|
||||
nc.nextID++
|
||||
|
@ -435,7 +392,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
|||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
clientConn, err := (&controlhttp.Dialer{
|
||||
|
|
|
@ -9,18 +9,19 @@ import (
|
|||
"net"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/tcpinfo"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (c *sclient) statsLoop(ctx context.Context) error {
|
||||
// Get the RTT initially to verify it's supported.
|
||||
conn := c.tcpConn()
|
||||
if conn == nil {
|
||||
// If we can't get a TCP socket, then we can't send stats.
|
||||
tcpConn := c.tcpConn()
|
||||
if tcpConn == nil {
|
||||
c.s.tcpRtt.Add("non-tcp", 1)
|
||||
return nil
|
||||
}
|
||||
if _, err := tcpinfo.RTT(conn); err != nil {
|
||||
c.logf("error fetching initial RTT: %v", err)
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
c.logf("error getting SyscallConn: %v", err)
|
||||
c.s.tcpRtt.Add("error", 1)
|
||||
return nil
|
||||
}
|
||||
|
@ -30,16 +31,23 @@ func (c *sclient) statsLoop(ctx context.Context) error {
|
|||
ticker := time.NewTicker(statsInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
var (
|
||||
tcpInfo *unix.TCPInfo
|
||||
sysErr error
|
||||
)
|
||||
statsLoop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
rtt, err := tcpinfo.RTT(conn)
|
||||
if err != nil {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
||||
})
|
||||
if err != nil || sysErr != nil {
|
||||
continue statsLoop
|
||||
}
|
||||
|
||||
// TODO(andrew): more metrics?
|
||||
rtt := time.Duration(tcpInfo.Rtt) * time.Microsecond
|
||||
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
|
||||
|
||||
case <-ctx.Done():
|
||||
|
|
|
@ -6,20 +6,22 @@ SA_NAME ?= tailscale
|
|||
TS_KUBE_SECRET ?= tailscale
|
||||
|
||||
rbac:
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml
|
||||
@echo "---"
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml
|
||||
@echo "---"
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
|
||||
|
||||
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:
|
||||
@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:
|
||||
@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:
|
||||
@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
|
||||
export SA_NAME=tailscale
|
||||
export TS_KUBE_SECRET=tailscale-auth
|
||||
make rbac | kubectl apply -f-
|
||||
make rbac
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
make sidecar | kubectl apply -f-
|
||||
make sidecar
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
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
|
||||
|
||||
```bash
|
||||
make userspace-sidecar | kubectl apply -f-
|
||||
make userspace-sidecar
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
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
|
||||
|
||||
```bash
|
||||
make proxy | kubectl apply -f-
|
||||
make proxy
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
kubectl logs proxy
|
||||
```
|
||||
|
@ -133,7 +133,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
|
|||
1. Deploy the subnet-router pod.
|
||||
|
||||
```bash
|
||||
make subnet-router | kubectl apply -f-
|
||||
make subnet-router
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
kubectl logs subnet-router
|
||||
```
|
||||
|
|
|
@ -742,6 +742,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
|||
HostName: p.Hostinfo.Hostname(),
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS(),
|
||||
KeepAlive: p.KeepAlive,
|
||||
LastSeen: lastSeen,
|
||||
Online: p.Online != nil && *p.Online,
|
||||
ShareeNode: p.Hostinfo.ShareeNode(),
|
||||
|
|
|
@ -223,8 +223,9 @@ type PeerStatus struct {
|
|||
LastSeen time.Time // last seen to tailcontrol; only present if offline
|
||||
LastHandshake time.Time // with local wireguard
|
||||
Online bool // whether node is connected to the control plane
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
||||
KeepAlive bool
|
||||
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
|
||||
// definition is somewhat undefined but has historically and
|
||||
|
@ -436,6 +437,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
|||
if st.InEngine {
|
||||
e.InEngine = true
|
||||
}
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ExitNode {
|
||||
e.ExitNode = true
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
|||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
|
||||
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/ee73d164e760/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
|
||||
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
|
||||
|
|
|
@ -31,7 +31,6 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
|||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
|
||||
|
@ -61,7 +60,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
|||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/dd4570e13977/LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
|
||||
|
|
|
@ -14,12 +14,10 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
|||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
|
||||
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/111c8c3b57c8/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
|
||||
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
|
||||
|
@ -34,12 +32,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
|||
- [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
|
||||
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f63dace725d8/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/59dfb47dfef1/LICENSE))
|
||||
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.0/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
|
||||
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
|
||||
|
@ -47,16 +42,14 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
|||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
|
||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/dd4570e13977/LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
|
||||
## Additional Dependencies
|
||||
|
|
|
@ -45,14 +45,6 @@ func (m *LabelMap) Get(key string) *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
|
||||
// if necessary.
|
||||
func (m *LabelMap) GetFloat(key string) *expvar.Float {
|
||||
|
|
|
@ -11,18 +11,6 @@ import (
|
|||
"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) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skipf("skipping on %v", runtime.GOOS)
|
||||
|
|
|
@ -36,9 +36,9 @@ func init() {
|
|||
}
|
||||
|
||||
func newResolver(tb testing.TB) *Resolver {
|
||||
clock := tstest.NewClock(tstest.ClockOpts{
|
||||
clock := &tstest.Clock{
|
||||
Step: 50 * time.Millisecond,
|
||||
})
|
||||
}
|
||||
return &Resolver{
|
||||
Logf: tb.Logf,
|
||||
timeNow: clock.Now,
|
||||
|
|
|
@ -18,9 +18,9 @@ import (
|
|||
)
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
mc := &MessageCache{Clock: clock.Now}
|
||||
mc.SetMaxCacheSize(2)
|
||||
clock.Advance(time.Second)
|
||||
|
|
|
@ -17,9 +17,16 @@ import (
|
|||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/linuxfw"
|
||||
)
|
||||
|
||||
// tailscaleBypassMark is the mark indicating that packets originating
|
||||
// from a socket should bypass Tailscale-managed routes during routing
|
||||
// table lookups.
|
||||
//
|
||||
// Keep this in sync with tailscaleBypassMark in
|
||||
// wgengine/router/router_linux.go.
|
||||
const tailscaleBypassMark = 0x80000
|
||||
|
||||
// socketMarkWorksOnce is the sync.Once & cached value for useSocketMark.
|
||||
var socketMarkWorksOnce struct {
|
||||
sync.Once
|
||||
|
@ -112,7 +119,7 @@ func controlC(network, address string, c syscall.RawConn) error {
|
|||
}
|
||||
|
||||
func setBypassMark(fd uintptr) error {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, linuxfw.TailscaleBypassMarkNum); err != nil {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, tailscaleBypassMark); err != nil {
|
||||
return fmt.Errorf("setting SO_MARK bypass: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -4,9 +4,51 @@
|
|||
package netns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// verifies tailscaleBypassMark is in sync with wgengine.
|
||||
func TestBypassMarkInSync(t *testing.T) {
|
||||
want := fmt.Sprintf("%q", fmt.Sprintf("0x%x", tailscaleBypassMark))
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "../../wgengine/router/router_linux.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, decl := range f.Decls {
|
||||
gd, ok := decl.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.CONST {
|
||||
continue
|
||||
}
|
||||
for _, spec := range gd.Specs {
|
||||
vs, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, ident := range vs.Names {
|
||||
if ident.Name != "tailscaleBypassMark" {
|
||||
continue
|
||||
}
|
||||
valExpr := vs.Values[i]
|
||||
lit, ok := valExpr.(*ast.BasicLit)
|
||||
if !ok {
|
||||
t.Errorf("tailscaleBypassMark = %T, expected *ast.BasicLit", valExpr)
|
||||
}
|
||||
if lit.Value == want {
|
||||
// Pass.
|
||||
return
|
||||
}
|
||||
t.Fatalf("router_linux.go's tailscaleBypassMark = %s; not in sync with netns's %s", lit.Value, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Errorf("tailscaleBypassMark not found in router_linux.go")
|
||||
}
|
||||
|
||||
func TestSocketMarkWorks(t *testing.T) {
|
||||
_ = socketMarkWorks()
|
||||
// we cannot actually assert whether the test runner has SO_MARK available
|
||||
|
|
|
@ -212,16 +212,9 @@ func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
|
|||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("couldn't parse %s: %w", k, err)
|
||||
}
|
||||
// 0 = disabled, 1 = enabled, 2 = enabled (but uncommon)
|
||||
// https://github.com/tailscale/tailscale/issues/8375
|
||||
if val < 0 || val > 2 {
|
||||
return false, fmt.Errorf("unexpected value %d for %s", val, k)
|
||||
}
|
||||
on := val == 1 || val == 2
|
||||
return on, nil
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tcpinfo provides platform-agnostic accessors to information about a
|
||||
// TCP connection (e.g. RTT, MSS, etc.).
|
||||
package tcpinfo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotTCP = errors.New("tcpinfo: not a TCP conn")
|
||||
ErrUnimplemented = errors.New("tcpinfo: unimplemented")
|
||||
)
|
||||
|
||||
// RTT returns the RTT for the given net.Conn.
|
||||
//
|
||||
// If the net.Conn is not a *net.TCPConn and cannot be unwrapped into one, then
|
||||
// ErrNotTCP will be returned. If retrieving the RTT is not supported on the
|
||||
// current platform, ErrUnimplemented will be returned.
|
||||
func RTT(conn net.Conn) (time.Duration, error) {
|
||||
tcpConn, err := unwrap(conn)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return rttImpl(tcpConn)
|
||||
}
|
||||
|
||||
// netConner is implemented by crypto/tls.Conn to unwrap into an underlying
|
||||
// net.Conn.
|
||||
type netConner interface {
|
||||
NetConn() net.Conn
|
||||
}
|
||||
|
||||
// unwrap attempts to unwrap a net.Conn into an underlying *net.TCPConn
|
||||
func unwrap(nc net.Conn) (*net.TCPConn, error) {
|
||||
for {
|
||||
switch v := nc.(type) {
|
||||
case *net.TCPConn:
|
||||
return v, nil
|
||||
case netConner:
|
||||
nc = v.NetConn()
|
||||
default:
|
||||
return nil, ErrNotTCP
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tcpinfo
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func rttImpl(conn *net.TCPConn) (time.Duration, error) {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
tcpInfo *unix.TCPConnectionInfo
|
||||
sysErr error
|
||||
)
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
tcpInfo, sysErr = unix.GetsockoptTCPConnectionInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_CONNECTION_INFO)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if sysErr != nil {
|
||||
return 0, sysErr
|
||||
}
|
||||
|
||||
return time.Duration(tcpInfo.Rttcur) * time.Millisecond, nil
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tcpinfo
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func rttImpl(conn *net.TCPConn) (time.Duration, error) {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
tcpInfo *unix.TCPInfo
|
||||
sysErr error
|
||||
)
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if sysErr != nil {
|
||||
return 0, sysErr
|
||||
}
|
||||
|
||||
return time.Duration(tcpInfo.Rtt) * time.Microsecond, nil
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package tcpinfo
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func rttImpl(conn *net.TCPConn) (time.Duration, error) {
|
||||
return 0, ErrUnimplemented
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tcpinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRTT(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
default:
|
||||
t.Skipf("not currently supported on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp4", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
|
||||
// Copy from the client to nowhere
|
||||
go io.Copy(io.Discard, c)
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp4", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write a bunch of data to the conn to force TCP session establishment
|
||||
// and a few packets.
|
||||
junkData := bytes.Repeat([]byte("hello world\n"), 1024*1024)
|
||||
for i := 0; i < 10; i++ {
|
||||
if _, err := conn.Write(junkData); err != nil {
|
||||
t.Fatalf("error writing junk data [%d]: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the RTT now
|
||||
rtt, err := RTT(conn)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting RTT: %v", err)
|
||||
}
|
||||
if rtt == 0 {
|
||||
t.Errorf("expected RTT > 0")
|
||||
}
|
||||
|
||||
t.Logf("TCP rtt: %v", rtt)
|
||||
}
|
|
@ -184,17 +184,6 @@ func (b *Build) TmpDir() string {
|
|||
// binary. Builds are cached by path and env, so each build only happens once
|
||||
// per process execution.
|
||||
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 {
|
||||
log.Printf("Initializing Go 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
|
||||
}
|
||||
|
||||
buildKey := []any{"go-build", path, env, tags}
|
||||
buildKey := []any{"go-build", path, env}
|
||||
return b.goBuilds.Do(buildKey, func() (string, error) {
|
||||
b.goBuildLimit <- struct{}{}
|
||||
defer func() { <-b.goBuildLimit }()
|
||||
|
@ -218,17 +207,9 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
|
|||
envStrs = append(envStrs, k+"="+v)
|
||||
}
|
||||
sort.Strings(envStrs)
|
||||
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " "))
|
||||
buildDir := b.TmpDir()
|
||||
args := []string{"build", "-v", "-o", buildDir}
|
||||
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...)
|
||||
cmd := b.Command(b.Repo, b.Go, "build", "-v", "-o", buildDir, path)
|
||||
for k, v := range env {
|
||||
cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v)
|
||||
}
|
||||
|
|
|
@ -59,36 +59,9 @@ func (m *ShardedMap[K, V]) Get(key K) (value V) {
|
|||
return
|
||||
}
|
||||
|
||||
// Mutate atomically mutates m[k] by calling mutator.
|
||||
//
|
||||
// The mutator function is called with the old value (or its zero value) and
|
||||
// whether it existed in the map and it returns the new value and whether it
|
||||
// should be set in the map (true) or deleted from the map (false).
|
||||
//
|
||||
// It returns the change in size of the map as a result of the mutation, one of
|
||||
// -1 (delete), 0 (change), or 1 (addition).
|
||||
func (m *ShardedMap[K, V]) Mutate(key K, mutator func(oldValue V, oldValueExisted bool) (newValue V, keep bool)) (sizeDelta int) {
|
||||
shard := m.shard(key)
|
||||
shard.mu.Lock()
|
||||
defer shard.mu.Unlock()
|
||||
oldV, oldOK := shard.m[key]
|
||||
newV, newOK := mutator(oldV, oldOK)
|
||||
if newOK {
|
||||
shard.m[key] = newV
|
||||
if oldOK {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
delete(shard.m, key)
|
||||
if oldOK {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Set sets m[key] = value.
|
||||
//
|
||||
// It reports whether the map grew in size (that is, whether key was not already
|
||||
// present in m).
|
||||
func (m *ShardedMap[K, V]) Set(key K, value V) (grew bool) {
|
||||
shard := m.shard(key)
|
||||
|
|
|
@ -41,41 +41,4 @@ func TestShardedMap(t *testing.T) {
|
|||
if g, w := m.Len(), 0; g != w {
|
||||
t.Errorf("got Len %v; want %v", g, w)
|
||||
}
|
||||
|
||||
// Mutation adding an entry.
|
||||
if v := m.Mutate(1, func(was string, ok bool) (string, bool) {
|
||||
if ok {
|
||||
t.Fatal("was okay")
|
||||
}
|
||||
return "ONE", true
|
||||
}); v != 1 {
|
||||
t.Errorf("Mutate = %v; want 1", v)
|
||||
}
|
||||
if g, w := m.Get(1), "ONE"; g != w {
|
||||
t.Errorf("got %q; want %q", g, w)
|
||||
}
|
||||
// Mutation changing an entry.
|
||||
if v := m.Mutate(1, func(was string, ok bool) (string, bool) {
|
||||
if !ok {
|
||||
t.Fatal("wasn't okay")
|
||||
}
|
||||
return was + "-" + was, true
|
||||
}); v != 0 {
|
||||
t.Errorf("Mutate = %v; want 0", v)
|
||||
}
|
||||
if g, w := m.Get(1), "ONE-ONE"; g != w {
|
||||
t.Errorf("got %q; want %q", g, w)
|
||||
}
|
||||
// Mutation removing an entry.
|
||||
if v := m.Mutate(1, func(was string, ok bool) (string, bool) {
|
||||
if !ok {
|
||||
t.Fatal("wasn't okay")
|
||||
}
|
||||
return "", false
|
||||
}); v != -1 {
|
||||
t.Errorf("Mutate = %v; want -1", v)
|
||||
}
|
||||
if g, w := m.Get(1), ""; g != w {
|
||||
t.Errorf("got %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,6 +242,8 @@ type Node struct {
|
|||
// current node doesn't have permission to know.
|
||||
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
|
||||
|
||||
// Capabilities are capabilities that the node has.
|
||||
|
@ -533,22 +535,15 @@ type Service struct {
|
|||
// Tailscale host. Location is optional and only set if
|
||||
// explicitly declared by a node.
|
||||
type Location struct {
|
||||
Country string `json:",omitempty"` // User friendly country name, with proper capitalization ("Canada")
|
||||
CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in upper case ("CA")
|
||||
City string `json:",omitempty"` // User friendly city name, with proper capitalization ("Squamish")
|
||||
Country string `json:",omitempty"` // User friendly country name, with proper capitalization, e.g "Canada"
|
||||
CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in lower case, e.g "ca"
|
||||
City string `json:",omitempty"` // User friendly city name, with proper capitalization, e.g. "Squamish"
|
||||
CityCode string `json:",omitempty"`
|
||||
|
||||
// CityCode is a short code representing the city in upper case.
|
||||
// CityCode is used to disambiguate a city from another location
|
||||
// with the same city name. It uniquely identifies a particular
|
||||
// geographical location, within the tailnet.
|
||||
// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE")
|
||||
CityCode string `json:",omitempty"`
|
||||
|
||||
// Priority determines the order of use of an exit node when a
|
||||
// location based preference matches more than one exit node,
|
||||
// the node with the highest priority wins. Nodes of equal
|
||||
// probability may be selected arbitrarily.
|
||||
//
|
||||
// Priority determines the priority an exit node is given when the
|
||||
// location data between two or more nodes is tied.
|
||||
// A higher value indicates that the exit node is more preferable
|
||||
// for use.
|
||||
// A value of 0 means the exit node does not have a priority
|
||||
// preference. A negative int is not allowed.
|
||||
Priority int `json:",omitempty"`
|
||||
|
@ -1282,7 +1277,7 @@ type DNSConfig struct {
|
|||
// match.
|
||||
//
|
||||
// Matches are case insensitive.
|
||||
ExitNodeFilteredSet []string `json:",omitempty"`
|
||||
ExitNodeFilteredSet []string
|
||||
}
|
||||
|
||||
// DNSRecord is an extra DNS record to add to MagicDNS.
|
||||
|
|
|
@ -93,6 +93,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
|||
PrimaryRoutes []netip.Prefix
|
||||
LastSeen *time.Time
|
||||
Online *bool
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []string
|
||||
UnsignedPeerAPIOnly bool
|
||||
|
|
|
@ -347,7 +347,7 @@ func TestNodeEqual(t *testing.T) {
|
|||
"Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey",
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "Cap", "Tags", "PrimaryRoutes",
|
||||
"LastSeen", "Online", "MachineAuthorized",
|
||||
"LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||
"Capabilities",
|
||||
"UnsignedPeerAPIOnly",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
|
|
|
@ -168,6 +168,7 @@ func (v NodeView) Online() *bool {
|
|||
return &x
|
||||
}
|
||||
|
||||
func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive }
|
||||
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
||||
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
|
||||
|
@ -209,6 +210,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
|||
PrimaryRoutes []netip.Prefix
|
||||
LastSeen *time.Time
|
||||
Online *bool
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []string
|
||||
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"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netns"
|
||||
|
@ -283,7 +282,6 @@ func TestConn(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoopbackLocalAPI(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557")
|
||||
tstest.ResourceCheck(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
@ -358,7 +356,6 @@ func TestLoopbackLocalAPI(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)
|
||||
defer cancel()
|
||||
|
||||
|
|
699
tstest/clock.go
699
tstest/clock.go
|
@ -4,686 +4,57 @@
|
|||
package tstest
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"sync"
|
||||
"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
|
||||
// called, beginning at its start time. If no start time is specified using
|
||||
// ClockBuilder, an arbitrary start time will be selected when the Clock is
|
||||
// created and can be retrieved by calling Clock.Start().
|
||||
// called, beginning at 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 {
|
||||
// start is the first value returned by Now. It must not be modified after
|
||||
// init is called.
|
||||
start time.Time
|
||||
// Start is the first value returned by Now.
|
||||
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
|
||||
// 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)
|
||||
})
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// Now returns the virtual clock's current time, and advances it
|
||||
// according to its step configuration.
|
||||
func (c *Clock) Now() time.Time {
|
||||
c.init()
|
||||
rt := c.maybeGetRealTime()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.initLocked()
|
||||
step := c.Step
|
||||
ret := c.Present
|
||||
c.Present = c.Present.Add(step)
|
||||
return ret
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
func (c *Clock) Advance(d time.Duration) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.initLocked()
|
||||
c.Present = c.Present.Add(d)
|
||||
}
|
||||
|
||||
step := c.step
|
||||
if c.skipStep {
|
||||
step = 0
|
||||
c.skipStep = false
|
||||
func (c *Clock) initLocked() {
|
||||
if c.Start.IsZero() {
|
||||
c.Start = time.Now()
|
||||
}
|
||||
c.advanceLocked(rt, step)
|
||||
|
||||
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)
|
||||
}
|
||||
if c.Present.Before(c.Start) {
|
||||
c.Present = c.Start
|
||||
}
|
||||
}
|
||||
|
||||
func (em *eventManager) updateTimerLocked() {
|
||||
if em.clock == nil {
|
||||
return
|
||||
}
|
||||
if len(em.heap) == 0 {
|
||||
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
|
||||
// Reset rewinds the virtual clock to its start time.
|
||||
func (c *Clock) Reset() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.Present = c.Start
|
||||
}
|
||||
|
|
2439
tstest/clock_test.go
2439
tstest/clock_test.go
File diff suppressed because it is too large
Load Diff
|
@ -42,7 +42,7 @@ func (panicLogWriter) Write(b []byte) (int, error) {
|
|||
// interfaces.GetState & tshttpproxy code to allow pushing
|
||||
// down a Logger yet. TODO(bradfitz): do that refactoring once
|
||||
// 1.2.0 is out.
|
||||
if bytes.Contains(b, []byte("tshttpproxy: ")) || bytes.Contains(b, []byte("runtime/panic.go:")) {
|
||||
if bytes.Contains(b, []byte("tshttpproxy: ")) {
|
||||
os.Stderr.Write(b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
|
|
@ -104,8 +104,7 @@ func (t Time) WallTime() time.Time {
|
|||
|
||||
// MarshalJSON formats t for JSON as if it were a time.Time.
|
||||
// We format Time this way for backwards-compatibility.
|
||||
// Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged
|
||||
// across different invocations of the Go process. This is best-effort only.
|
||||
// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged.
|
||||
// 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.
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
|
@ -114,8 +113,7 @@ func (t Time) MarshalJSON() ([]byte, error) {
|
|||
}
|
||||
|
||||
// UnmarshalJSON sets t according to data.
|
||||
// Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged
|
||||
// across different invocations of the Go process. This is best-effort only.
|
||||
// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged.
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
var tt time.Time
|
||||
err := tt.UnmarshalJSON(data)
|
||||
|
@ -126,6 +124,6 @@ func (t *Time) UnmarshalJSON(data []byte) error {
|
|||
*t = 0
|
||||
return nil
|
||||
}
|
||||
*t = baseMono.Add(tt.Sub(baseWall))
|
||||
*t = Now().Add(-time.Since(tt))
|
||||
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) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
|
@ -59,79 +59,3 @@ func Sleep(ctx context.Context, d time.Duration) bool {
|
|||
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")
|
||||
bgCtx = context.Background()
|
||||
// canceledCtx, cancel = context.WithCancel(bgCtx)
|
||||
startTime = time.Unix(1687870000, 1234)
|
||||
clock = tstest.Clock{
|
||||
Start: time.Now(),
|
||||
Step: time.Second,
|
||||
}
|
||||
)
|
||||
// cancel()
|
||||
|
||||
|
@ -83,7 +86,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/"),
|
||||
wantCode: 200,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
TLS: false,
|
||||
|
@ -100,7 +103,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 404,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -116,7 +119,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 404,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -133,7 +136,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 404,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -150,7 +153,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 500,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -167,7 +170,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 500,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -184,7 +187,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 500,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -201,7 +204,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 200,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -218,7 +221,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 200,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -235,7 +238,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 200,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
|
@ -257,7 +260,7 @@ func TestStdHandler(t *testing.T) {
|
|||
r: req(bgCtx, "http://example.com/foo"),
|
||||
wantCode: 200,
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
|
||||
Proto: "HTTP/1.1",
|
||||
|
@ -276,7 +279,7 @@ func TestStdHandler(t *testing.T) {
|
|||
http.Error(w, e.Msg, 200)
|
||||
},
|
||||
wantLog: AccessLogRecord{
|
||||
When: startTime,
|
||||
When: clock.Start,
|
||||
Seconds: 1.0,
|
||||
Proto: "HTTP/1.1",
|
||||
TLS: false,
|
||||
|
@ -299,10 +302,7 @@ func TestStdHandler(t *testing.T) {
|
|||
t.Logf(fmt, args...)
|
||||
}
|
||||
|
||||
clock := tstest.NewClock(tstest.ClockOpts{
|
||||
Start: startTime,
|
||||
Step: time.Second,
|
||||
})
|
||||
clock.Reset()
|
||||
|
||||
rec := noopHijacker{httptest.NewRecorder(), false}
|
||||
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
|
||||
// keys. The output ordering is deterministic.
|
||||
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:
|
||||
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",
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"counter_labelmap_keyname_m",
|
||||
|
|
|
@ -10,12 +10,11 @@ import (
|
|||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"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 {
|
||||
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.ж) }
|
||||
|
||||
// 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.
|
||||
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.
|
||||
func (v *Slice[T]) UnmarshalJSON(b []byte) error {
|
||||
return unmarshalSliceFromJSON(b, &v.ж)
|
||||
return unmarshalJSON(b, &v.ж)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Implementations should return false to stop range.
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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\x00",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||
|
||||
package linuxfw
|
||||
|
||||
|
|
|
@ -1,475 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
type iptablesInterface interface {
|
||||
// Adding this interface for testing purposes so we can mock out
|
||||
// the iptables library, in reality this is a wrapper to *iptables.IPTables.
|
||||
Insert(table, chain string, pos int, args ...string) error
|
||||
Append(table, chain string, args ...string) error
|
||||
Exists(table, chain string, args ...string) (bool, error)
|
||||
Delete(table, chain string, args ...string) error
|
||||
ClearChain(table, chain string) error
|
||||
NewChain(table, chain string) error
|
||||
DeleteChain(table, chain string) error
|
||||
}
|
||||
|
||||
type iptablesRunner struct {
|
||||
ipt4 iptablesInterface
|
||||
ipt6 iptablesInterface
|
||||
|
||||
v6Available bool
|
||||
v6NATAvailable bool
|
||||
}
|
||||
|
||||
// NewIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
|
||||
// If the underlying iptables library fails to initialize, that error is
|
||||
// returned. The runner probes for IPv6 support once at initialization time and
|
||||
// if not found, no IPv6 rules will be modified for the lifetime of the runner.
|
||||
func NewIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supportsV6, supportsV6NAT := false, false
|
||||
v6err := checkIPv6(logf)
|
||||
if v6err != nil {
|
||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||
} else {
|
||||
supportsV6 = true
|
||||
supportsV6NAT = supportsV6 && checkSupportsV6NAT()
|
||||
logf("v6nat = %v", supportsV6NAT)
|
||||
}
|
||||
|
||||
var ipt6 *iptables.IPTables
|
||||
if supportsV6 {
|
||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil
|
||||
}
|
||||
|
||||
// HasIPV6 returns true if the system supports IPv6.
|
||||
func (i *iptablesRunner) HasIPV6() bool {
|
||||
return i.v6Available
|
||||
}
|
||||
|
||||
// HasIPV6NAT returns true if the system supports IPv6 NAT.
|
||||
func (i *iptablesRunner) HasIPV6NAT() bool {
|
||||
return i.v6NATAvailable
|
||||
}
|
||||
|
||||
func isErrChainNotExist(err error) bool {
|
||||
return errCode(err) == 1
|
||||
}
|
||||
|
||||
// getIPTByAddr returns the iptablesInterface with correct IP family
|
||||
// that we will be using for the given address.
|
||||
func (i *iptablesRunner) getIPTByAddr(addr netip.Addr) iptablesInterface {
|
||||
nf := i.ipt4
|
||||
if addr.Is6() {
|
||||
nf = i.ipt6
|
||||
}
|
||||
return nf
|
||||
}
|
||||
|
||||
// AddLoopbackRule adds an iptables rule to permit loopback traffic to
|
||||
// a local Tailscale IP.
|
||||
func (i *iptablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsChain returns the name of the tailscale sub-chain corresponding
|
||||
// to the given "parent" chain (e.g. INPUT, FORWARD, ...).
|
||||
func tsChain(chain string) string {
|
||||
return "ts-" + strings.ToLower(chain)
|
||||
}
|
||||
|
||||
// DelLoopbackRule removes the iptables rule permitting loopback
|
||||
// traffic to a Tailscale IP.
|
||||
func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTables gets the available iptablesInterface in iptables runner.
|
||||
func (i *iptablesRunner) getTables() []iptablesInterface {
|
||||
if i.HasIPV6() {
|
||||
return []iptablesInterface{i.ipt4, i.ipt6}
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// getNATTables gets the available iptablesInterface in iptables runner.
|
||||
// If the system does not support IPv6 NAT, only the IPv4 iptablesInterface
|
||||
// is returned.
|
||||
func (i *iptablesRunner) getNATTables() []iptablesInterface {
|
||||
if i.HasIPV6NAT() {
|
||||
return i.getTables()
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// AddHooks inserts calls to tailscale's netfilter chains in
|
||||
// the relevant main netfilter chains. The tailscale chains must
|
||||
// already exist. If they do not, an error is returned.
|
||||
func (i *iptablesRunner) AddHooks() error {
|
||||
// divert inserts a jump to the tailscale chain in the given table/chain.
|
||||
// If the jump already exists, it is a no-op.
|
||||
divert := func(ipt iptablesInterface, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
|
||||
args := []string{"-j", tsChain}
|
||||
exists, err := ipt.Exists(table, chain, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := ipt.Insert(table, chain, 1, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := divert(ipt, "filter", "INPUT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := divert(ipt, "filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := divert(ipt, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddChains creates custom Tailscale chains in netfilter via iptables
|
||||
// if the ts-chain doesn't already exist.
|
||||
func (i *iptablesRunner) AddChains() error {
|
||||
// create creates a chain in the given table if it doesn't already exist.
|
||||
// If the chain already exists, it is a no-op.
|
||||
create := func(ipt iptablesInterface, table, chain string) error {
|
||||
err := ipt.ClearChain(table, chain)
|
||||
if isErrChainNotExist(err) {
|
||||
// nonexistent chain. let's create it!
|
||||
return ipt.NewChain(table, chain)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := create(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := create(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := create(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBase adds some basic processing rules to be supplemented by
|
||||
// later calls to other helpers.
|
||||
func (i *iptablesRunner) AddBase(tunname string) error {
|
||||
if err := i.addBase4(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
if i.HasIPV6() {
|
||||
if err := i.addBase6(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addBase4 adds some basic IPv6 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase4(tunname string) error {
|
||||
// Only allow CGNAT range traffic to come from tailscale0. There
|
||||
// is an exception carved out for ranges used by ChromeOS, for
|
||||
// which we fall out of the Tailscale chain.
|
||||
//
|
||||
// Note, this will definitely break nodes that end up using the
|
||||
// CGNAT range for other purposes :(.
|
||||
args := []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
args = []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Forward all traffic from the Tailscale interface, and drop
|
||||
// traffic to the tailscale interface by default. We use packet
|
||||
// marks here so both filter/FORWARD and nat/POSTROUTING can match
|
||||
// on these packets of interest.
|
||||
//
|
||||
// In particular, we only want to apply SNAT rules in
|
||||
// nat/POSTROUTING to packets that originated from the Tailscale
|
||||
// interface, but we can't match on the inbound interface in
|
||||
// POSTROUTING. So instead, we match on the inbound interface in
|
||||
// filter/FORWARD, and set a packet mark that nat/POSTROUTING can
|
||||
// use to effectively run that same test again.
|
||||
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addBase6 adds some basic IPv4 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase6(tunname string) error {
|
||||
// TODO: only allow traffic from Tailscale's ULA range to come
|
||||
// from tailscale0.
|
||||
|
||||
args := []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
|
||||
// (see corresponding IPv4 CGNAT rule).
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelChains removes the custom Tailscale chains from netfilter via iptables.
|
||||
func (i *iptablesRunner) DelChains() error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelBase empties but does not remove custom Tailscale chains from
|
||||
// netfilter via iptables.
|
||||
func (i *iptablesRunner) DelBase() error {
|
||||
del := func(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isErrChainNotExist(err) {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := del(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := del(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHooks deletes the calls to tailscale's netfilter chains
|
||||
// in the relevant main netfilter chains.
|
||||
func (i *iptablesRunner) DelHooks(logf logger.Logf) error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSNATRule adds a netfilter rule to SNAT traffic destined for
|
||||
// local subnets.
|
||||
func (i *iptablesRunner) AddSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Append("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelSNATRule removes the netfilter rule to SNAT traffic destined for
|
||||
// local subnets. An error is returned if the rule does not exist.
|
||||
func (i *iptablesRunner) DelSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Delete("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPTablesCleanup removes all Tailscale added iptables rules.
|
||||
// Any errors that occur are logged to the provided logf.
|
||||
func IPTablesCleanup(logf logger.Logf) {
|
||||
err := clearRules(iptables.ProtocolIPv4, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear iptables: %v", err)
|
||||
}
|
||||
|
||||
err = clearRules(iptables.ProtocolIPv6, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear ip6tables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delTSHook deletes hook in a chain that jumps to a ts-chain. If the hook does not
|
||||
// exist, it's a no-op since the desired state is already achieved but we log the
|
||||
// error because error code from the iptables module resists unwrapping.
|
||||
func delTSHook(ipt iptablesInterface, table, chain string, logf logger.Logf) error {
|
||||
tsChain := tsChain(chain)
|
||||
args := []string{"-j", tsChain}
|
||||
if err := ipt.Delete(table, chain, args...); err != nil {
|
||||
// TODO(apenwarr): check for errCode(1) here.
|
||||
// Unfortunately the error code from the iptables
|
||||
// module resists unwrapping, unlike with other
|
||||
// calls. So we have to assume if Delete fails,
|
||||
// it's because there is no such rule.
|
||||
logf("deleting %v in %s/%s: %v", args, table, chain, err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// delChain flushs and deletes a chain. If the chain does not exist, it's a no-op
|
||||
// since the desired state is already achieved. otherwise, it returns an error.
|
||||
func delChain(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isErrChainNotExist(err) {
|
||||
// nonexistent chain. nothing to do.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
if err := ipt.DeleteChain(table, chain); err != nil {
|
||||
return fmt.Errorf("deleting %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearRules clears all the iptables rules created by Tailscale
|
||||
// for the given protocol. If error occurs, it's logged but not returned.
|
||||
func clearRules(proto iptables.Protocol, logf logger.Logf) error {
|
||||
ipt, err := iptables.NewWithProtocol(proto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return multierr.New(errs...)
|
||||
}
|
|
@ -1,420 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var errExec = errors.New("execution failed")
|
||||
|
||||
type fakeIPTables struct {
|
||||
t *testing.T
|
||||
n map[string][]string
|
||||
}
|
||||
|
||||
type fakeRule struct {
|
||||
table, chain string
|
||||
args []string
|
||||
}
|
||||
|
||||
func newIPTables(t *testing.T) *fakeIPTables {
|
||||
return &fakeIPTables{
|
||||
t: t,
|
||||
n: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
"nat/PREROUTING": nil,
|
||||
"nat/OUTPUT": nil,
|
||||
"nat/POSTROUTING": nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Insert(table, chain string, pos int, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if pos > len(rules)+1 {
|
||||
n.t.Errorf("bad position %d in %s", pos, k)
|
||||
return errExec
|
||||
}
|
||||
rules = append(rules, "")
|
||||
copy(rules[pos:], rules[pos-1:])
|
||||
rules[pos-1] = strings.Join(args, " ")
|
||||
n.n[k] = rules
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return errExec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Append(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
return n.Insert(table, chain, len(n.n[k])+1, args...)
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Exists(table, chain string, args ...string) (bool, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for _, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else {
|
||||
n.t.Logf("unknown table/chain %s", k)
|
||||
return false, errExec
|
||||
}
|
||||
}
|
||||
|
||||
func hasChain(n *fakeIPTables, table, chain string) bool {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Delete(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for i, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
rules = append(rules[:i], rules[i+1:]...)
|
||||
n.n[k] = rules
|
||||
return nil
|
||||
}
|
||||
}
|
||||
n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
|
||||
return errExec
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return errExec
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) ClearChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
} else {
|
||||
n.t.Logf("note: ClearChain: unknown table/chain %s", k)
|
||||
return errors.New("exitcode:1")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) NewChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.t.Errorf("table/chain %s already exists", k)
|
||||
return errExec
|
||||
}
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) DeleteChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if len(rules) != 0 {
|
||||
n.t.Errorf("%s is not empty", k)
|
||||
return errExec
|
||||
}
|
||||
delete(n.n, k)
|
||||
return nil
|
||||
} else {
|
||||
n.t.Errorf("%s does not exist", k)
|
||||
return errExec
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeIPTablesRunner(t *testing.T) *iptablesRunner {
|
||||
ipt4 := newIPTables(t)
|
||||
ipt6 := newIPTables(t)
|
||||
|
||||
iptr := &iptablesRunner{ipt4, ipt6, true, true}
|
||||
return iptr
|
||||
}
|
||||
|
||||
func TestAddAndDeleteChains(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
err := iptr.AddChains()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the chains were created.
|
||||
tsChains := []struct{ table, chain string }{ // table/chain
|
||||
{"filter", "ts-input"},
|
||||
{"filter", "ts-forward"},
|
||||
{"nat", "ts-postrouting"},
|
||||
}
|
||||
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tc := range tsChains {
|
||||
// Exists returns error if the chain doesn't exist.
|
||||
if _, err := proto.Exists(tc.table, tc.chain); err != nil {
|
||||
t.Errorf("chain %s/%s doesn't exist", tc.table, tc.chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = iptr.DelChains()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the chains were deleted.
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tc := range tsChains {
|
||||
if _, err = proto.Exists(tc.table, tc.chain); err == nil {
|
||||
t.Errorf("chain %s/%s still exists", tc.table, tc.chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddAndDeleteHooks(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
// don't need to test what happens if the chains don't exist, because
|
||||
// this is handled by fake iptables, in realife iptables would return error.
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer iptr.DelChains()
|
||||
|
||||
if err := iptr.AddHooks(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were created.
|
||||
tsRules := []fakeRule{ // table/chain/rule
|
||||
{"filter", "INPUT", []string{"-j", "ts-input"}},
|
||||
{"filter", "FORWARD", []string{"-j", "ts-forward"}},
|
||||
{"nat", "POSTROUTING", []string{"-j", "ts-postrouting"}},
|
||||
}
|
||||
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tr := range tsRules {
|
||||
if exists, err := proto.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exists {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
// check if the rule is at front of the chain
|
||||
if proto.(*fakeIPTables).n[tr.table+"/"+tr.chain][0] != strings.Join(tr.args, " ") {
|
||||
t.Errorf("v4 rule %s/%s/%s is not at the top", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelHooks(t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were deleted.
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tr := range tsRules {
|
||||
if exists, err := proto.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exists {
|
||||
t.Errorf("rule %s/%s/%s still exists", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.AddHooks(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndDeleteBase(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
tunname := "tun0"
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := iptr.AddBase(tunname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were created.
|
||||
tsRulesV4 := []fakeRule{ // table/chain/rule
|
||||
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}},
|
||||
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
||||
{"filter", "ts-forward", []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
||||
}
|
||||
|
||||
tsRulesCommon := []fakeRule{ // table/chain/rule
|
||||
{"filter", "ts-forward", []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}},
|
||||
{"filter", "ts-forward", []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}},
|
||||
{"filter", "ts-forward", []string{"-o", tunname, "-j", "ACCEPT"}},
|
||||
}
|
||||
|
||||
// check that the rules were created for ipt4
|
||||
for _, tr := range append(tsRulesV4, tsRulesCommon...) {
|
||||
if exists, err := iptr.ipt4.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exists {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// check that the rules were created for ipt6
|
||||
for _, tr := range tsRulesCommon {
|
||||
if exists, err := iptr.ipt6.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exists {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelBase(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were deleted.
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tr := range append(tsRulesV4, tsRulesCommon...) {
|
||||
if exists, err := proto.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exists {
|
||||
t.Errorf("rule %s/%s/%s still exists", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndDelLoopbackRule(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
// We don't need to test for malformed addresses, AddLoopbackRule
|
||||
// takes in a netip.Addr, which is already valid.
|
||||
fakeAddrV4 := netip.MustParseAddr("192.168.0.2")
|
||||
fakeAddrV6 := netip.MustParseAddr("2001:db8::2")
|
||||
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.AddLoopbackRule(fakeAddrV4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.AddLoopbackRule(fakeAddrV6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were created.
|
||||
tsRulesV4 := fakeRule{ // table/chain/rule
|
||||
"filter", "ts-input", []string{"-i", "lo", "-s", fakeAddrV4.String(), "-j", "ACCEPT"}}
|
||||
|
||||
tsRulesV6 := fakeRule{ // table/chain/rule
|
||||
"filter", "ts-input", []string{"-i", "lo", "-s", fakeAddrV6.String(), "-j", "ACCEPT"}}
|
||||
|
||||
// check that the rules were created for ipt4 and ipt6
|
||||
if exist, err := iptr.ipt4.Exists(tsRulesV4.table, tsRulesV4.chain, tsRulesV4.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exist {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tsRulesV4.table, tsRulesV4.chain, strings.Join(tsRulesV4.args, " "))
|
||||
}
|
||||
if exist, err := iptr.ipt6.Exists(tsRulesV6.table, tsRulesV6.chain, tsRulesV6.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exist {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tsRulesV6.table, tsRulesV6.chain, strings.Join(tsRulesV6.args, " "))
|
||||
}
|
||||
|
||||
// check that the rule is at the top
|
||||
chain := "filter/ts-input"
|
||||
if iptr.ipt4.(*fakeIPTables).n[chain][0] != strings.Join(tsRulesV4.args, " ") {
|
||||
t.Errorf("v4 rule %s/%s/%s is not at the top", tsRulesV4.table, tsRulesV4.chain, strings.Join(tsRulesV4.args, " "))
|
||||
}
|
||||
if iptr.ipt6.(*fakeIPTables).n[chain][0] != strings.Join(tsRulesV6.args, " ") {
|
||||
t.Errorf("v6 rule %s/%s/%s is not at the top", tsRulesV6.table, tsRulesV6.chain, strings.Join(tsRulesV6.args, " "))
|
||||
}
|
||||
|
||||
// delete the rules
|
||||
if err := iptr.DelLoopbackRule(fakeAddrV4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.DelLoopbackRule(fakeAddrV6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were deleted.
|
||||
if exist, err := iptr.ipt4.Exists(tsRulesV4.table, tsRulesV4.chain, tsRulesV4.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exist {
|
||||
t.Errorf("rule %s/%s/%s still exists", tsRulesV4.table, tsRulesV4.chain, strings.Join(tsRulesV4.args, " "))
|
||||
}
|
||||
|
||||
if exist, err := iptr.ipt6.Exists(tsRulesV6.table, tsRulesV6.chain, tsRulesV6.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exist {
|
||||
t.Errorf("rule %s/%s/%s still exists", tsRulesV6.table, tsRulesV6.chain, strings.Join(tsRulesV6.args, " "))
|
||||
}
|
||||
|
||||
if err := iptr.DelChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndDelSNATRule(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rule := fakeRule{ // table/chain/rule
|
||||
"nat", "ts-postrouting", []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"},
|
||||
}
|
||||
|
||||
// Add SNAT rule
|
||||
if err := iptr.AddSNATRule(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rule was created for ipt4 and ipt6
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
if exist, err := proto.Exists(rule.table, rule.chain, rule.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exist {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", rule.table, rule.chain, strings.Join(rule.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete SNAT rule
|
||||
if err := iptr.DelSNATRule(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rule was deleted for ipt4 and ipt6
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
if exist, err := proto.Exists(rule.table, rule.chain, rule.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exist {
|
||||
t.Errorf("rule %s/%s/%s still exists", rule.table, rule.chain, strings.Join(rule.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -2,179 +2,10 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package linuxfw returns the kind of firewall being used by the kernel.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
import "errors"
|
||||
|
||||
"github.com/tailscale/netlink"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// The following bits are added to packet marks for Tailscale use.
|
||||
//
|
||||
// We tried to pick bits sufficiently out of the way that it's
|
||||
// unlikely to collide with existing uses. We have 4 bytes of mark
|
||||
// bits to play with. We leave the lower byte alone on the assumption
|
||||
// that sysadmins would use those. Kubernetes uses a few bits in the
|
||||
// second byte, so we steer clear of that too.
|
||||
//
|
||||
// Empirically, most of the documentation on packet marks on the
|
||||
// internet gives the impression that the marks are 16 bits
|
||||
// wide. Based on this, we theorize that the upper two bytes are
|
||||
// relatively unused in the wild, and so we consume bits 16:23 (the
|
||||
// third byte).
|
||||
//
|
||||
// The constants are in the iptables/iproute2 string format for
|
||||
// matching and setting the bits, so they can be directly embedded in
|
||||
// commands.
|
||||
const (
|
||||
// The mask for reading/writing the 'firewall mask' bits on a packet.
|
||||
// See the comment on the const block on why we only use the third byte.
|
||||
//
|
||||
// We claim bits 16:23 entirely. For now we only use the lower four
|
||||
// bits, leaving the higher 4 bits for future use.
|
||||
TailscaleFwmarkMask = "0xff0000"
|
||||
TailscaleFwmarkMaskNeg = "0xff00ffff"
|
||||
TailscaleFwmarkMaskNum = 0xff0000
|
||||
|
||||
// Packet is from Tailscale and to a subnet route destination, so
|
||||
// is allowed to be routed through this machine.
|
||||
TailscaleSubnetRouteMark = "0x40000"
|
||||
TailscaleSubnetRouteMarkNum = 0x40000
|
||||
// This one is same value but padded to even number of digit, so
|
||||
// hex decoding can work correctly.
|
||||
TailscaleSubnetRouteMarkHexStr = "0x040000"
|
||||
|
||||
// Packet was originated by tailscaled itself, and must not be
|
||||
// routed over the Tailscale network.
|
||||
TailscaleBypassMark = "0x80000"
|
||||
TailscaleBypassMarkNum = 0x80000
|
||||
)
|
||||
|
||||
// errCode extracts and returns the process exit code from err, or
|
||||
// zero if err is nil.
|
||||
func errCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
var e *exec.ExitError
|
||||
if ok := errors.As(err, &e); ok {
|
||||
return e.ExitCode()
|
||||
}
|
||||
s := err.Error()
|
||||
if strings.HasPrefix(s, "exitcode:") {
|
||||
code, err := strconv.Atoi(s[9:])
|
||||
if err == nil {
|
||||
return code
|
||||
}
|
||||
}
|
||||
return -42
|
||||
}
|
||||
|
||||
// checkIPv6 checks whether the system appears to have a working IPv6
|
||||
// network stack. It returns an error explaining what looks wrong or
|
||||
// missing. It does not check that IPv6 is currently functional or
|
||||
// that there's a global address, just that the system would support
|
||||
// IPv6 if it were on an IPv6 network.
|
||||
func checkIPv6(logf logger.Logf) error {
|
||||
_, err := os.Stat("/proc/sys/net/ipv6")
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
bs, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
if err != nil {
|
||||
// Be conservative if we can't find the IPv6 configuration knob.
|
||||
return err
|
||||
}
|
||||
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_ipv6 has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_ipv6 is set")
|
||||
}
|
||||
|
||||
// Older kernels don't support IPv6 policy routing. Some kernels
|
||||
// support policy routing but don't have this knob, so absence of
|
||||
// the knob is not fatal.
|
||||
bs, err = os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
|
||||
if err == nil {
|
||||
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_policy has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_policy is set")
|
||||
}
|
||||
}
|
||||
|
||||
if err := CheckIPRuleSupportsV6(logf); err != nil {
|
||||
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
|
||||
}
|
||||
|
||||
// Some distros ship ip6tables separately from iptables.
|
||||
if _, err := exec.LookPath("ip6tables"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSupportsV6NAT returns whether the system has a "nat" table in the
|
||||
// IPv6 netfilter stack.
|
||||
//
|
||||
// The nat table was added after the initial release of ipv6
|
||||
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
||||
// traffic.
|
||||
func checkSupportsV6NAT() bool {
|
||||
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
||||
if err != nil {
|
||||
// Can't read the file. Assume SNAT works.
|
||||
return true
|
||||
}
|
||||
if bytes.Contains(bs, []byte("nat\n")) {
|
||||
return true
|
||||
}
|
||||
// In nftables mode, that proc file will be empty. Try another thing:
|
||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckIPRuleSupportsV6(logf logger.Logf) error {
|
||||
// First try just a read-only operation to ideally avoid
|
||||
// having to modify any state.
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
|
||||
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
|
||||
} else {
|
||||
if len(rules) > 0 {
|
||||
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to actually create & delete one as a test.
|
||||
rule := netlink.NewRule()
|
||||
rule.Priority = 1234
|
||||
rule.Mark = TailscaleBypassMarkNum
|
||||
rule.Table = 52
|
||||
rule.Family = netlink.FAMILY_V6
|
||||
// First delete the rule unconditionally, and don't check for
|
||||
// errors. This is just cleaning up anything that might be already
|
||||
// there.
|
||||
netlink.RuleDel(rule)
|
||||
// And clean up on exit.
|
||||
defer netlink.RuleDel(rule)
|
||||
return netlink.RuleAdd(rule)
|
||||
}
|
||||
// ErrUnsupported is the error returned from all functions on non-Linux
|
||||
// platforms.
|
||||
var ErrUnsupported = errors.New("unsupported")
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||
|
||||
package linuxfw
|
||||
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// 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.
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build !linux || (linux && !(arm64 || amd64))
|
||||
//go:build !linux || (linux && (386 || loong64 || arm || armbe))
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// ErrUnsupported is the error returned from all functions on non-Linux
|
||||
// platforms.
|
||||
var ErrUnsupported = errors.New("linuxfw:unsupported")
|
||||
|
||||
// DebugNetfilter is not supported on non-Linux platforms.
|
||||
func DebugNetfilter(logf logger.Logf) error {
|
||||
return ErrUnsupported
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||
|
||||
package linuxfw
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
//go:build linux && !(386 || loong64 || arm || armbe)
|
||||
|
||||
package linuxfw
|
||||
|
||||
|
|
110
util/lru/lru.go
110
util/lru/lru.go
|
@ -1,110 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package lru contains a typed Least-Recently-Used cache.
|
||||
package lru
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
)
|
||||
|
||||
// Cache is container type keyed by K, storing V, optionally evicting the least
|
||||
// recently used items if a maximum size is exceeded.
|
||||
//
|
||||
// The zero value is valid to use.
|
||||
//
|
||||
// It is not safe for concurrent access.
|
||||
//
|
||||
// The current implementation is just the traditional LRU linked list; a future
|
||||
// implementation may be more advanced to avoid pathological cases.
|
||||
type Cache[K comparable, V any] struct {
|
||||
// MaxEntries is the maximum number of cache entries before
|
||||
// an item is evicted. Zero means no limit.
|
||||
MaxEntries int
|
||||
|
||||
ll *list.List
|
||||
m map[K]*list.Element // of *entry[K,V]
|
||||
}
|
||||
|
||||
// entry is the element type for the container/list.Element.
|
||||
type entry[K comparable, V any] struct {
|
||||
key K
|
||||
value V
|
||||
}
|
||||
|
||||
// Set adds or replaces a value to the cache, set or updating its associated
|
||||
// value.
|
||||
//
|
||||
// If MaxEntries is non-zero and the length of the cache is greater
|
||||
// after any addition, the least recently used value is evicted.
|
||||
func (c *Cache[K, V]) Set(key K, value V) {
|
||||
if c.m == nil {
|
||||
c.m = make(map[K]*list.Element)
|
||||
c.ll = list.New()
|
||||
}
|
||||
if ee, ok := c.m[key]; ok {
|
||||
c.ll.MoveToFront(ee)
|
||||
ee.Value.(*entry[K, V]).value = value
|
||||
return
|
||||
}
|
||||
ele := c.ll.PushFront(&entry[K, V]{key, value})
|
||||
c.m[key] = ele
|
||||
if c.MaxEntries != 0 && c.Len() > c.MaxEntries {
|
||||
c.DeleteOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache, returning either
|
||||
// the value or the zero value if it not present.
|
||||
//
|
||||
// If found, key is moved to the front of the LRU.
|
||||
func (c *Cache[K, V]) Get(key K) V {
|
||||
v, _ := c.GetOk(key)
|
||||
return v
|
||||
}
|
||||
|
||||
// Contains reports whether c contains key.
|
||||
//
|
||||
// If found, key is moved to the front of the LRU.
|
||||
func (c *Cache[K, V]) Contains(key K) bool {
|
||||
_, ok := c.GetOk(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetOk looks up a key's value from the cache, also reporting
|
||||
// whether it was present.
|
||||
//
|
||||
// If found, key is moved to the front of the LRU.
|
||||
func (c *Cache[K, V]) GetOk(key K) (value V, ok bool) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.ll.MoveToFront(ele)
|
||||
return ele.Value.(*entry[K, V]).value, true
|
||||
}
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Delete removes the provided key from the cache if it was present.
|
||||
func (c *Cache[K, V]) Delete(key K) {
|
||||
if e, ok := c.m[key]; ok {
|
||||
c.deleteElement(e)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteOldest removes the item from the cache that was least recently
|
||||
// accessed. It is a no-op if the cache is empty.
|
||||
func (c *Cache[K, V]) DeleteOldest() {
|
||||
if c.ll != nil {
|
||||
if e := c.ll.Back(); e != nil {
|
||||
c.deleteElement(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) deleteElement(e *list.Element) {
|
||||
c.ll.Remove(e)
|
||||
delete(c.m, e.Value.(*entry[K, V]).key)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache[K, V]) Len() int { return len(c.m) }
|
|
@ -1,42 +0,0 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package lru
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLRU(t *testing.T) {
|
||||
var c Cache[int, string]
|
||||
c.Set(1, "one")
|
||||
c.Set(2, "two")
|
||||
if g, w := c.Get(1), "one"; g != w {
|
||||
t.Errorf("got %q; want %q", g, w)
|
||||
}
|
||||
if g, w := c.Get(2), "two"; g != w {
|
||||
t.Errorf("got %q; want %q", g, w)
|
||||
}
|
||||
c.DeleteOldest()
|
||||
if g, w := c.Get(1), ""; g != w {
|
||||
t.Errorf("got %q; want %q", g, w)
|
||||
}
|
||||
if g, w := c.Len(), 1; g != w {
|
||||
t.Errorf("Len = %d; want %d", g, w)
|
||||
}
|
||||
c.MaxEntries = 2
|
||||
c.Set(1, "one")
|
||||
c.Set(2, "two")
|
||||
c.Set(3, "three")
|
||||
if c.Contains(1) {
|
||||
t.Errorf("contains 1; should not")
|
||||
}
|
||||
if !c.Contains(2) {
|
||||
t.Errorf("doesn't contain 2; should")
|
||||
}
|
||||
if !c.Contains(3) {
|
||||
t.Errorf("doesn't contain 3; should")
|
||||
}
|
||||
c.Delete(3)
|
||||
if c.Contains(3) {
|
||||
t.Errorf("contains 3; should not")
|
||||
}
|
||||
}
|
|
@ -88,7 +88,7 @@ func IsAppleTV() bool {
|
|||
return false
|
||||
}
|
||||
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"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
"tailscale.com/control/controlclient"
|
||||
|
@ -4410,12 +4409,16 @@ func (de *endpoint) addrForWireGuardSendLocked(now mono.Time) (udpAddr netip.Add
|
|||
return udpAddr, false
|
||||
}
|
||||
|
||||
candidates := maps.Keys(de.endpointState)
|
||||
if len(candidates) == 0 {
|
||||
de.c.logf("magicsock: addrForSendWireguardLocked: [unexpected] no candidates available for endpoint")
|
||||
return udpAddr, false
|
||||
candidates := make([]netip.AddrPort, 0, len(de.endpointState))
|
||||
for ipp := range de.endpointState {
|
||||
if ipp.Addr().Is4() && de.c.noV4.Load() {
|
||||
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
|
||||
// and give it a short trustBestAddrUntil time so we avoid flapping between
|
||||
// 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"),
|
||||
},
|
||||
{
|
||||
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",
|
||||
sendWGPing: true,
|
||||
|
@ -2835,6 +2865,8 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
|
|||
noV6: atomic.Bool{},
|
||||
},
|
||||
}
|
||||
endpoint.c.noV4.Store(test.noV4)
|
||||
endpoint.c.noV6.Store(test.noV6)
|
||||
|
||||
for _, epd := range test.ep {
|
||||
endpoint.endpointState[epd.addrPort] = &endpointState{}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"github.com/tailscale/netlink"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"go4.org/netipx"
|
||||
|
@ -23,9 +25,9 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/linuxfw"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
@ -36,34 +38,56 @@ const (
|
|||
netfilterOn = preftype.NetfilterOn
|
||||
)
|
||||
|
||||
// The following bits are added to packet marks for Tailscale use.
|
||||
//
|
||||
// We tried to pick bits sufficiently out of the way that it's
|
||||
// unlikely to collide with existing uses. We have 4 bytes of mark
|
||||
// bits to play with. We leave the lower byte alone on the assumption
|
||||
// that sysadmins would use those. Kubernetes uses a few bits in the
|
||||
// second byte, so we steer clear of that too.
|
||||
//
|
||||
// Empirically, most of the documentation on packet marks on the
|
||||
// internet gives the impression that the marks are 16 bits
|
||||
// wide. Based on this, we theorize that the upper two bytes are
|
||||
// relatively unused in the wild, and so we consume bits 16:23 (the
|
||||
// third byte).
|
||||
//
|
||||
// The constants are in the iptables/iproute2 string format for
|
||||
// matching and setting the bits, so they can be directly embedded in
|
||||
// commands.
|
||||
const (
|
||||
// The mask for reading/writing the 'firewall mask' bits on a packet.
|
||||
// See the comment on the const block on why we only use the third byte.
|
||||
//
|
||||
// We claim bits 16:23 entirely. For now we only use the lower four
|
||||
// bits, leaving the higher 4 bits for future use.
|
||||
tailscaleFwmarkMask = "0xff0000"
|
||||
tailscaleFwmarkMaskNum = 0xff0000
|
||||
|
||||
// Packet is from Tailscale and to a subnet route destination, so
|
||||
// is allowed to be routed through this machine.
|
||||
tailscaleSubnetRouteMark = "0x40000"
|
||||
|
||||
// Packet was originated by tailscaled itself, and must not be
|
||||
// routed over the Tailscale network.
|
||||
//
|
||||
// Keep this in sync with tailscaleBypassMark in
|
||||
// net/netns/netns_linux.go.
|
||||
tailscaleBypassMark = "0x80000"
|
||||
tailscaleBypassMarkNum = 0x80000
|
||||
)
|
||||
|
||||
// netfilterRunner abstracts helpers to run netfilter commands. It
|
||||
// exists purely to swap out go-iptables for a fake implementation in
|
||||
// tests.
|
||||
type netfilterRunner interface {
|
||||
AddLoopbackRule(addr netip.Addr) error
|
||||
DelLoopbackRule(addr netip.Addr) error
|
||||
AddHooks() error
|
||||
DelHooks(logf logger.Logf) error
|
||||
AddChains() error
|
||||
DelChains() error
|
||||
AddBase(tunname string) error
|
||||
DelBase() error
|
||||
AddSNATRule() error
|
||||
DelSNATRule() error
|
||||
|
||||
HasIPV6() bool
|
||||
HasIPV6NAT() bool
|
||||
}
|
||||
|
||||
func newNetfilterRunner(logf logger.Logf) (netfilterRunner, error) {
|
||||
var nfr netfilterRunner
|
||||
var err error
|
||||
nfr, err = linuxfw.NewIPTablesRunner(logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nfr, nil
|
||||
Insert(table, chain string, pos int, args ...string) error
|
||||
Append(table, chain string, args ...string) error
|
||||
Exists(table, chain string, args ...string) (bool, error)
|
||||
Delete(table, chain string, args ...string) error
|
||||
ClearChain(table, chain string) error
|
||||
NewChain(table, chain string) error
|
||||
DeleteChain(table, chain string) error
|
||||
}
|
||||
|
||||
type linuxRouter struct {
|
||||
|
@ -85,13 +109,16 @@ type linuxRouter struct {
|
|||
|
||||
// Various feature checks for the network stack.
|
||||
ipRuleAvailable bool // whether kernel was built with IP_MULTIPLE_TABLES
|
||||
v6Available bool
|
||||
v6NATAvailable bool
|
||||
fwmaskWorks bool // whether we can use 'ip rule...fwmark <mark>/<mask>'
|
||||
|
||||
// ipPolicyPrefBase is the base priority at which ip rules are installed.
|
||||
ipPolicyPrefBase int
|
||||
|
||||
nfr netfilterRunner
|
||||
cmd commandRunner
|
||||
ipt4 netfilterRunner
|
||||
ipt6 netfilterRunner
|
||||
cmd commandRunner
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
|
@ -100,27 +127,51 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
|
|||
return nil, err
|
||||
}
|
||||
|
||||
nfr, err := newNetfilterRunner(logf)
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v6err := checkIPv6(logf)
|
||||
if v6err != nil {
|
||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||
}
|
||||
supportsV6 := v6err == nil
|
||||
supportsV6NAT := supportsV6 && supportsV6NAT()
|
||||
if supportsV6 {
|
||||
logf("v6nat = %v", supportsV6NAT)
|
||||
}
|
||||
|
||||
var ipt6 netfilterRunner
|
||||
if supportsV6 {
|
||||
// The iptables package probes for `ip6tables` and errors out
|
||||
// if unavailable. We want that to be a non-fatal error.
|
||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cmd := osCommandRunner{
|
||||
ambientCapNetAdmin: useAmbientCaps(),
|
||||
}
|
||||
|
||||
return newUserspaceRouterAdvanced(logf, tunname, netMon, nfr, cmd)
|
||||
return newUserspaceRouterAdvanced(logf, tunname, netMon, ipt4, ipt6, cmd, supportsV6, supportsV6NAT)
|
||||
}
|
||||
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr netfilterRunner, cmd commandRunner) (Router, error) {
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, netfilter4, netfilter6 netfilterRunner, cmd commandRunner, supportsV6, supportsV6NAT bool) (Router, error) {
|
||||
r := &linuxRouter{
|
||||
logf: logf,
|
||||
tunname: tunname,
|
||||
netfilterMode: netfilterOff,
|
||||
netMon: netMon,
|
||||
|
||||
nfr: nfr,
|
||||
cmd: cmd,
|
||||
v6Available: supportsV6,
|
||||
v6NATAvailable: supportsV6NAT,
|
||||
|
||||
ipt4: netfilter4,
|
||||
ipt6: netfilter6,
|
||||
cmd: cmd,
|
||||
|
||||
ipRuleFixLimiter: rate.NewLimiter(rate.Every(5*time.Second), 10),
|
||||
ipPolicyPrefBase: 5200,
|
||||
|
@ -433,23 +484,23 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
|
|||
case netfilterOff:
|
||||
switch r.netfilterMode {
|
||||
case netfilterNoDivert:
|
||||
if err := r.nfr.DelBase(); err != nil {
|
||||
if err := r.delNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.DelChains(); err != nil {
|
||||
if err := r.delNetfilterChains(); err != nil {
|
||||
r.logf("note: %v", err)
|
||||
// harmless, continue.
|
||||
// This can happen if someone left a ref to
|
||||
// this table somewhere else.
|
||||
}
|
||||
case netfilterOn:
|
||||
if err := r.nfr.DelHooks(r.logf); err != nil {
|
||||
if err := r.delNetfilterHooks(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.DelBase(); err != nil {
|
||||
if err := r.delNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.DelChains(); err != nil {
|
||||
if err := r.delNetfilterChains(); err != nil {
|
||||
r.logf("note: %v", err)
|
||||
// harmless, continue.
|
||||
// This can happen if someone left a ref to
|
||||
|
@ -461,15 +512,15 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
|
|||
switch r.netfilterMode {
|
||||
case netfilterOff:
|
||||
reprocess = true
|
||||
if err := r.nfr.AddChains(); err != nil {
|
||||
if err := r.addNetfilterChains(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.AddBase(r.tunname); err != nil {
|
||||
if err := r.addNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.snatSubnetRoutes = false
|
||||
case netfilterOn:
|
||||
if err := r.nfr.DelHooks(r.logf); err != nil {
|
||||
if err := r.delNetfilterHooks(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -478,33 +529,33 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
|
|||
// we can't add a "-j ts-forward" rule to FORWARD
|
||||
// while ts-forward contains an "-m mark" rule. But
|
||||
// we can add the row *before* populating ts-forward.
|
||||
// So we have to delBase, then add the hooks,
|
||||
// then re-addBase, just in case.
|
||||
// So we have to delNetFilterBase, then add the hooks,
|
||||
// then re-addNetFilterBase, just in case.
|
||||
switch r.netfilterMode {
|
||||
case netfilterOff:
|
||||
reprocess = true
|
||||
if err := r.nfr.AddChains(); err != nil {
|
||||
if err := r.addNetfilterChains(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.DelBase(); err != nil {
|
||||
if err := r.delNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.AddHooks(); err != nil {
|
||||
if err := r.addNetfilterHooks(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.AddBase(r.tunname); err != nil {
|
||||
if err := r.addNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.snatSubnetRoutes = false
|
||||
case netfilterNoDivert:
|
||||
reprocess = true
|
||||
if err := r.nfr.DelBase(); err != nil {
|
||||
if err := r.delNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.AddHooks(); err != nil {
|
||||
if err := r.addNetfilterHooks(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.nfr.AddBase(r.tunname); err != nil {
|
||||
if err := r.addNetfilterBase(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.snatSubnetRoutes = false
|
||||
|
@ -528,19 +579,11 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *linuxRouter) getV6Available() bool {
|
||||
return r.nfr.HasIPV6()
|
||||
}
|
||||
|
||||
func (r *linuxRouter) getV6NATAvailable() bool {
|
||||
return r.nfr.HasIPV6NAT()
|
||||
}
|
||||
|
||||
// addAddress adds an IP/mask to the tunnel interface. Fails if the
|
||||
// address is already assigned to the interface, or if the addition
|
||||
// fails.
|
||||
func (r *linuxRouter) addAddress(addr netip.Prefix) error {
|
||||
if !r.getV6Available() && addr.Addr().Is6() {
|
||||
if !r.v6Available && addr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
if r.useIPCommand() {
|
||||
|
@ -566,7 +609,7 @@ func (r *linuxRouter) addAddress(addr netip.Prefix) error {
|
|||
// the address is not assigned to the interface, or if the removal
|
||||
// fails.
|
||||
func (r *linuxRouter) delAddress(addr netip.Prefix) error {
|
||||
if !r.getV6Available() && addr.Addr().Is6() {
|
||||
if !r.v6Available && addr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.delLoopbackRule(addr.Addr()); err != nil {
|
||||
|
@ -595,8 +638,17 @@ func (r *linuxRouter) addLoopbackRule(addr netip.Addr) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := r.nfr.AddLoopbackRule(addr); err != nil {
|
||||
return err
|
||||
nf := r.ipt4
|
||||
if addr.Is6() {
|
||||
if !r.v6Available {
|
||||
// IPv6 not available, ignore.
|
||||
return nil
|
||||
}
|
||||
nf = r.ipt6
|
||||
}
|
||||
|
||||
if err := nf.Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -608,8 +660,17 @@ func (r *linuxRouter) delLoopbackRule(addr netip.Addr) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := r.nfr.DelLoopbackRule(addr); err != nil {
|
||||
return err
|
||||
nf := r.ipt4
|
||||
if addr.Is6() {
|
||||
if !r.v6Available {
|
||||
// IPv6 not available, ignore.
|
||||
return nil
|
||||
}
|
||||
nf = r.ipt6
|
||||
}
|
||||
|
||||
if err := nf.Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -618,7 +679,7 @@ func (r *linuxRouter) delLoopbackRule(addr netip.Addr) error {
|
|||
// interface. Fails if the route already exists, or if adding the
|
||||
// route fails.
|
||||
func (r *linuxRouter) addRoute(cidr netip.Prefix) error {
|
||||
if !r.getV6Available() && cidr.Addr().Is6() {
|
||||
if !r.v6Available && cidr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
if r.useIPCommand() {
|
||||
|
@ -643,7 +704,7 @@ func (r *linuxRouter) addThrowRoute(cidr netip.Prefix) error {
|
|||
if !r.ipRuleAvailable {
|
||||
return nil
|
||||
}
|
||||
if !r.getV6Available() && cidr.Addr().Is6() {
|
||||
if !r.v6Available && cidr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
if r.useIPCommand() {
|
||||
|
@ -651,7 +712,7 @@ func (r *linuxRouter) addThrowRoute(cidr netip.Prefix) error {
|
|||
}
|
||||
err := netlink.RouteReplace(&netlink.Route{
|
||||
Dst: netipx.PrefixIPNet(cidr.Masked()),
|
||||
Table: tailscaleRouteTable.Num,
|
||||
Table: tailscaleRouteTable.num,
|
||||
Type: unix.RTN_THROW,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -661,7 +722,7 @@ func (r *linuxRouter) addThrowRoute(cidr netip.Prefix) error {
|
|||
}
|
||||
|
||||
func (r *linuxRouter) addRouteDef(routeDef []string, cidr netip.Prefix) error {
|
||||
if !r.getV6Available() && cidr.Addr().Is6() {
|
||||
if !r.v6Available && cidr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
args := append([]string{"ip", "route", "add"}, routeDef...)
|
||||
|
@ -695,7 +756,7 @@ var (
|
|||
// interface. Fails if the route doesn't exist, or if removing the
|
||||
// route fails.
|
||||
func (r *linuxRouter) delRoute(cidr netip.Prefix) error {
|
||||
if !r.getV6Available() && cidr.Addr().Is6() {
|
||||
if !r.v6Available && cidr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
if r.useIPCommand() {
|
||||
|
@ -723,7 +784,7 @@ func (r *linuxRouter) delThrowRoute(cidr netip.Prefix) error {
|
|||
if !r.ipRuleAvailable {
|
||||
return nil
|
||||
}
|
||||
if !r.getV6Available() && cidr.Addr().Is6() {
|
||||
if !r.v6Available && cidr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
if r.useIPCommand() {
|
||||
|
@ -742,7 +803,7 @@ func (r *linuxRouter) delThrowRoute(cidr netip.Prefix) error {
|
|||
}
|
||||
|
||||
func (r *linuxRouter) delRouteDef(routeDef []string, cidr netip.Prefix) error {
|
||||
if !r.getV6Available() && cidr.Addr().Is6() {
|
||||
if !r.v6Available && cidr.Addr().Is6() {
|
||||
return nil
|
||||
}
|
||||
args := append([]string{"ip", "route", "del"}, routeDef...)
|
||||
|
@ -804,7 +865,7 @@ func (r *linuxRouter) linkIndex() (int, error) {
|
|||
// routeTable returns the route table to use.
|
||||
func (r *linuxRouter) routeTable() int {
|
||||
if r.ipRuleAvailable {
|
||||
return tailscaleRouteTable.Num
|
||||
return tailscaleRouteTable.num
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@ -901,7 +962,7 @@ func (f addrFamily) netlinkInt() int {
|
|||
}
|
||||
|
||||
func (r *linuxRouter) addrFamilies() []addrFamily {
|
||||
if r.getV6Available() {
|
||||
if r.v6Available {
|
||||
return []addrFamily{v4, v6}
|
||||
}
|
||||
return []addrFamily{v4}
|
||||
|
@ -924,34 +985,30 @@ func (r *linuxRouter) addIPRules() error {
|
|||
return r.justAddIPRules()
|
||||
}
|
||||
|
||||
// RouteTable is a Linux routing table: both its name and number.
|
||||
// routeTable is a Linux routing table: both its name and number.
|
||||
// See /etc/iproute2/rt_tables.
|
||||
type RouteTable struct {
|
||||
Name string
|
||||
Num int
|
||||
type routeTable struct {
|
||||
name string
|
||||
num int
|
||||
}
|
||||
|
||||
var routeTableByNumber = map[int]RouteTable{}
|
||||
|
||||
// IpCmdArg returns the string form of the table to pass to the "ip" command.
|
||||
func (rt RouteTable) ipCmdArg() string {
|
||||
if rt.Num >= 253 {
|
||||
return rt.Name
|
||||
// ipCmdArg returns the string form of the table to pass to the "ip" command.
|
||||
func (rt routeTable) ipCmdArg() string {
|
||||
if rt.num >= 253 {
|
||||
return rt.name
|
||||
}
|
||||
return strconv.Itoa(rt.Num)
|
||||
return strconv.Itoa(rt.num)
|
||||
}
|
||||
|
||||
func newRouteTable(name string, num int) RouteTable {
|
||||
rt := RouteTable{name, num}
|
||||
var routeTableByNumber = map[int]routeTable{}
|
||||
|
||||
func newRouteTable(name string, num int) routeTable {
|
||||
rt := routeTable{name, num}
|
||||
routeTableByNumber[num] = rt
|
||||
return rt
|
||||
}
|
||||
|
||||
// MustRouteTable returns the RouteTable with the given number key.
|
||||
// It panics if the number is unknown because this result is a part
|
||||
// of IP rule argument and we don't want to continue with an invalid
|
||||
// argument with table no exist.
|
||||
func mustRouteTable(num int) RouteTable {
|
||||
func mustRouteTable(num int) routeTable {
|
||||
rt, ok := routeTableByNumber[num]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown route table %v", num))
|
||||
|
@ -1002,22 +1059,22 @@ var ipRules = []netlink.Rule{
|
|||
// main routing table.
|
||||
{
|
||||
Priority: 10,
|
||||
Mark: linuxfw.TailscaleBypassMarkNum,
|
||||
Table: mainRouteTable.Num,
|
||||
Mark: tailscaleBypassMarkNum,
|
||||
Table: mainRouteTable.num,
|
||||
},
|
||||
// ...and then we try the 'default' table, for correctness,
|
||||
// even though it's been empty on every Linux system I've ever seen.
|
||||
{
|
||||
Priority: 30,
|
||||
Mark: linuxfw.TailscaleBypassMarkNum,
|
||||
Table: defaultRouteTable.Num,
|
||||
Mark: tailscaleBypassMarkNum,
|
||||
Table: defaultRouteTable.num,
|
||||
},
|
||||
// If neither of those matched (no default route on this system?)
|
||||
// then packets from us should be aborted rather than falling through
|
||||
// to the tailscale routes, because that would create routing loops.
|
||||
{
|
||||
Priority: 50,
|
||||
Mark: linuxfw.TailscaleBypassMarkNum,
|
||||
Mark: tailscaleBypassMarkNum,
|
||||
Type: unix.RTN_UNREACHABLE,
|
||||
},
|
||||
// If we get to this point, capture all packets and send them
|
||||
|
@ -1027,7 +1084,7 @@ var ipRules = []netlink.Rule{
|
|||
// beat non-VPN routes.
|
||||
{
|
||||
Priority: 70,
|
||||
Table: tailscaleRouteTable.Num,
|
||||
Table: tailscaleRouteTable.num,
|
||||
},
|
||||
// If that didn't match, then non-fwmark packets fall through to the
|
||||
// usual rules (pref 32766 and 32767, ie. main and default).
|
||||
|
@ -1048,7 +1105,7 @@ func (r *linuxRouter) justAddIPRules() error {
|
|||
// Note: r is a value type here; safe to mutate it.
|
||||
ru.Family = family.netlinkInt()
|
||||
if ru.Mark != 0 {
|
||||
ru.Mask = linuxfw.TailscaleFwmarkMaskNum
|
||||
ru.Mask = tailscaleFwmarkMaskNum
|
||||
}
|
||||
ru.Goto = -1
|
||||
ru.SuppressIfgroup = -1
|
||||
|
@ -1081,7 +1138,7 @@ func (r *linuxRouter) addIPRulesWithIPCommand() error {
|
|||
}
|
||||
if rule.Mark != 0 {
|
||||
if r.fwmaskWorks {
|
||||
args = append(args, "fwmark", fmt.Sprintf("0x%x/%s", rule.Mark, linuxfw.TailscaleFwmarkMask))
|
||||
args = append(args, "fwmark", fmt.Sprintf("0x%x/%s", rule.Mark, tailscaleFwmarkMask))
|
||||
} else {
|
||||
args = append(args, "fwmark", fmt.Sprintf("0x%x", rule.Mark))
|
||||
}
|
||||
|
@ -1182,6 +1239,284 @@ func (r *linuxRouter) delIPRulesWithIPCommand() error {
|
|||
return rg.ErrAcc
|
||||
}
|
||||
|
||||
func (r *linuxRouter) netfilterFamilies() []netfilterRunner {
|
||||
if r.v6Available {
|
||||
return []netfilterRunner{r.ipt4, r.ipt6}
|
||||
}
|
||||
return []netfilterRunner{r.ipt4}
|
||||
}
|
||||
|
||||
// addNetfilterChains creates custom Tailscale chains in netfilter.
|
||||
func (r *linuxRouter) addNetfilterChains() error {
|
||||
create := func(ipt netfilterRunner, table, chain string) error {
|
||||
err := ipt.ClearChain(table, chain)
|
||||
if errCode(err) == 1 {
|
||||
// nonexistent chain. let's create it!
|
||||
return ipt.NewChain(table, chain)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range r.netfilterFamilies() {
|
||||
if err := create(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := create(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := create(r.ipt4, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := create(r.ipt6, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNetfilterBase adds some basic processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (r *linuxRouter) addNetfilterBase() error {
|
||||
if err := r.addNetfilterBase4(); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.v6Available {
|
||||
if err := r.addNetfilterBase6(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNetfilterBase4 adds some basic IPv4 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (r *linuxRouter) addNetfilterBase4() error {
|
||||
// Only allow CGNAT range traffic to come from tailscale0. There
|
||||
// is an exception carved out for ranges used by ChromeOS, for
|
||||
// which we fall out of the Tailscale chain.
|
||||
//
|
||||
// Note, this will definitely break nodes that end up using the
|
||||
// CGNAT range for other purposes :(.
|
||||
args := []string{"!", "-i", r.tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
|
||||
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
args = []string{"!", "-i", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Forward all traffic from the Tailscale interface, and drop
|
||||
// traffic to the tailscale interface by default. We use packet
|
||||
// marks here so both filter/FORWARD and nat/POSTROUTING can match
|
||||
// on these packets of interest.
|
||||
//
|
||||
// In particular, we only want to apply SNAT rules in
|
||||
// nat/POSTROUTING to packets that originated from the Tailscale
|
||||
// interface, but we can't match on the inbound interface in
|
||||
// POSTROUTING. So instead, we match on the inbound interface in
|
||||
// filter/FORWARD, and set a packet mark that nat/POSTROUTING can
|
||||
// use to effectively run that same test again.
|
||||
args = []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark + "/" + tailscaleFwmarkMask}
|
||||
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark + "/" + tailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", r.tunname, "-j", "ACCEPT"}
|
||||
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNetfilterBase4 adds some basic IPv6 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (r *linuxRouter) addNetfilterBase6() error {
|
||||
// TODO: only allow traffic from Tailscale's ULA range to come
|
||||
// from tailscale0.
|
||||
|
||||
args := []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark + "/" + tailscaleFwmarkMask}
|
||||
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark + "/" + tailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
|
||||
// (see corresponding IPv4 CGNAT rule).
|
||||
args = []string{"-o", r.tunname, "-j", "ACCEPT"}
|
||||
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// delNetfilterChains removes the custom Tailscale chains from netfilter.
|
||||
func (r *linuxRouter) delNetfilterChains() error {
|
||||
del := func(ipt netfilterRunner, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if errCode(err) == 1 {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
if err := ipt.DeleteChain(table, chain); err != nil {
|
||||
// this shouldn't fail, because if the chain didn't
|
||||
// exist, we would have returned after ClearChain.
|
||||
return fmt.Errorf("deleting %s/%s: %v", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range r.netfilterFamilies() {
|
||||
if err := del(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// delNetfilterBase empties but does not remove custom Tailscale chains from
|
||||
// netfilter.
|
||||
func (r *linuxRouter) delNetfilterBase() error {
|
||||
del := func(ipt netfilterRunner, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if errCode(err) == 1 {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range r.netfilterFamilies() {
|
||||
if err := del(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNetfilterHooks inserts calls to tailscale's netfilter chains in
|
||||
// the relevant main netfilter chains. The tailscale chains must
|
||||
// already exist.
|
||||
func (r *linuxRouter) addNetfilterHooks() error {
|
||||
divert := func(ipt netfilterRunner, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
|
||||
args := []string{"-j", tsChain}
|
||||
exists, err := ipt.Exists(table, chain, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := ipt.Insert(table, chain, 1, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range r.netfilterFamilies() {
|
||||
if err := divert(ipt, "filter", "INPUT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := divert(ipt, "filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := divert(r.ipt4, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := divert(r.ipt6, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// delNetfilterHooks deletes the calls to tailscale's netfilter chains
|
||||
// in the relevant main netfilter chains.
|
||||
func (r *linuxRouter) delNetfilterHooks() error {
|
||||
del := func(ipt netfilterRunner, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
args := []string{"-j", tsChain}
|
||||
if err := ipt.Delete(table, chain, args...); err != nil {
|
||||
// TODO(apenwarr): check for errCode(1) here.
|
||||
// Unfortunately the error code from the iptables
|
||||
// module resists unwrapping, unlike with other
|
||||
// calls. So we have to assume if Delete fails,
|
||||
// it's because there is no such rule.
|
||||
r.logf("note: deleting %v in %s/%s: %w", args, table, chain, err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range r.netfilterFamilies() {
|
||||
if err := del(ipt, "filter", "INPUT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := del(r.ipt4, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := del(r.ipt6, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addSNATRule adds a netfilter rule to SNAT traffic destined for
|
||||
// local subnets.
|
||||
func (r *linuxRouter) addSNATRule() error {
|
||||
|
@ -1189,8 +1524,14 @@ func (r *linuxRouter) addSNATRule() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := r.nfr.AddSNATRule(); err != nil {
|
||||
return err
|
||||
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark + "/" + tailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
if err := r.ipt4.Append("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := r.ipt6.Append("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1202,8 +1543,14 @@ func (r *linuxRouter) delSNATRule() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := r.nfr.DelSNATRule(); err != nil {
|
||||
return err
|
||||
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark + "/" + tailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
if err := r.ipt4.Delete("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in v4/nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
if r.v6NATAvailable {
|
||||
if err := r.ipt6.Delete("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in v6/nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1272,6 +1619,12 @@ func cidrDiff(kind string, old map[netip.Prefix]bool, new []netip.Prefix, add, d
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
// tsChain returns the name of the tailscale sub-chain corresponding
|
||||
// to the given "parent" chain (e.g. INPUT, FORWARD, ...).
|
||||
func tsChain(chain string) string {
|
||||
return "ts-" + strings.ToLower(chain)
|
||||
}
|
||||
|
||||
// normalizeCIDR returns cidr as an ip/mask string, with the host bits
|
||||
// of the IP address zeroed out.
|
||||
func normalizeCIDR(cidr netip.Prefix) string {
|
||||
|
@ -1279,9 +1632,105 @@ func normalizeCIDR(cidr netip.Prefix) string {
|
|||
}
|
||||
|
||||
func cleanup(logf logger.Logf, interfaceName string) {
|
||||
if interfaceName != "userspace-networking" {
|
||||
linuxfw.IPTablesCleanup(logf)
|
||||
// TODO(dmytro): clean up iptables.
|
||||
}
|
||||
|
||||
// checkIPv6 checks whether the system appears to have a working IPv6
|
||||
// network stack. It returns an error explaining what looks wrong or
|
||||
// missing. It does not check that IPv6 is currently functional or
|
||||
// that there's a global address, just that the system would support
|
||||
// IPv6 if it were on an IPv6 network.
|
||||
func checkIPv6(logf logger.Logf) error {
|
||||
_, err := os.Stat("/proc/sys/net/ipv6")
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
bs, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
if err != nil {
|
||||
// Be conservative if we can't find the IPv6 configuration knob.
|
||||
return err
|
||||
}
|
||||
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_ipv6 has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_ipv6 is set")
|
||||
}
|
||||
|
||||
// Older kernels don't support IPv6 policy routing. Some kernels
|
||||
// support policy routing but don't have this knob, so absence of
|
||||
// the knob is not fatal.
|
||||
bs, err = os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
|
||||
if err == nil {
|
||||
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_policy has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_policy is set")
|
||||
}
|
||||
}
|
||||
|
||||
if err := checkIPRuleSupportsV6(logf); err != nil {
|
||||
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
|
||||
}
|
||||
|
||||
// Some distros ship ip6tables separately from iptables.
|
||||
if _, err := exec.LookPath("ip6tables"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// supportsV6NAT returns whether the system has a "nat" table in the
|
||||
// IPv6 netfilter stack.
|
||||
//
|
||||
// The nat table was added after the initial release of ipv6
|
||||
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
||||
// traffic.
|
||||
func supportsV6NAT() bool {
|
||||
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
||||
if err != nil {
|
||||
// Can't read the file. Assume SNAT works.
|
||||
return true
|
||||
}
|
||||
if bytes.Contains(bs, []byte("nat\n")) {
|
||||
return true
|
||||
}
|
||||
// In nftables mode, that proc file will be empty. Try another thing:
|
||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIPRuleSupportsV6(logf logger.Logf) error {
|
||||
// First try just a read-only operation to ideally avoid
|
||||
// having to modify any state.
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
|
||||
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
|
||||
} else {
|
||||
if len(rules) > 0 {
|
||||
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to actually create & delete one as a test.
|
||||
rule := netlink.NewRule()
|
||||
rule.Priority = 1234
|
||||
rule.Mark = tailscaleBypassMarkNum
|
||||
rule.Table = tailscaleRouteTable.num
|
||||
rule.Family = netlink.FAMILY_V6
|
||||
// First delete the rule unconditionally, and don't check for
|
||||
// errors. This is just cleaning up anything that might be already
|
||||
// there.
|
||||
netlink.RuleDel(rule)
|
||||
// And clean up on exit.
|
||||
defer netlink.RuleDel(rule)
|
||||
return netlink.RuleAdd(rule)
|
||||
}
|
||||
|
||||
// Checks if the running openWRT system is using mwan3, based on the heuristic
|
||||
|
|
|
@ -22,10 +22,8 @@ import (
|
|||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/linuxfw"
|
||||
)
|
||||
|
||||
func TestRouterStates(t *testing.T) {
|
||||
|
@ -330,7 +328,7 @@ ip route add throw 192.168.0.0/24 table 52` + basic,
|
|||
defer mon.Close()
|
||||
|
||||
fake := NewFakeOS(t)
|
||||
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", mon, fake.nfr, fake)
|
||||
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", mon, fake.netfilter4, fake.netfilter6, fake, true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create router: %v", err)
|
||||
}
|
||||
|
@ -364,25 +362,15 @@ ip route add throw 192.168.0.0/24 table 52` + basic,
|
|||
}
|
||||
}
|
||||
|
||||
type fakeIPTablesRunner struct {
|
||||
t *testing.T
|
||||
ipt4 map[string][]string
|
||||
ipt6 map[string][]string
|
||||
//we always assume ipv6 and ipv6 nat are enabled when testing
|
||||
type fakeNetfilter struct {
|
||||
t *testing.T
|
||||
n map[string][]string
|
||||
}
|
||||
|
||||
func newIPTablesRunner(t *testing.T) netfilterRunner {
|
||||
return &fakeIPTablesRunner{
|
||||
func newNetfilter(t *testing.T) *fakeNetfilter {
|
||||
return &fakeNetfilter{
|
||||
t: t,
|
||||
ipt4: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
"nat/PREROUTING": nil,
|
||||
"nat/OUTPUT": nil,
|
||||
"nat/POSTROUTING": nil,
|
||||
},
|
||||
ipt6: map[string][]string{
|
||||
n: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
|
@ -393,222 +381,115 @@ func newIPTablesRunner(t *testing.T) netfilterRunner {
|
|||
}
|
||||
}
|
||||
|
||||
func insertRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRule string) error {
|
||||
// Get current rules for filter/ts-input chain with according IP version
|
||||
curTSInputRules, ok := curIPT[chain]
|
||||
if !ok {
|
||||
n.t.Fatalf("no %s chain exists", chain)
|
||||
return fmt.Errorf("no %s chain exists", chain)
|
||||
}
|
||||
|
||||
// Add new rule to top of filter/ts-input
|
||||
curTSInputRules = append(curTSInputRules, "")
|
||||
copy(curTSInputRules[1:], curTSInputRules)
|
||||
curTSInputRules[0] = newRule
|
||||
curIPT[chain] = curTSInputRules
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRule string) error {
|
||||
// Get current rules for filter/ts-input chain with according IP version
|
||||
curTSInputRules, ok := curIPT[chain]
|
||||
if !ok {
|
||||
n.t.Fatalf("no %s chain exists", chain)
|
||||
return fmt.Errorf("no %s chain exists", chain)
|
||||
}
|
||||
|
||||
// Add new rule to end of filter/ts-input
|
||||
curTSInputRules = append(curTSInputRules, newRule)
|
||||
curIPT[chain] = curTSInputRules
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, delRule string) error {
|
||||
// Get current rules for filter/ts-input chain with according IP version
|
||||
curTSInputRules, ok := curIPT[chain]
|
||||
if !ok {
|
||||
n.t.Fatalf("no %s chain exists", chain)
|
||||
return fmt.Errorf("no %s chain exists", chain)
|
||||
}
|
||||
|
||||
// Remove rule from filter/ts-input
|
||||
for i, rule := range curTSInputRules {
|
||||
if rule == delRule {
|
||||
curTSInputRules = append(curTSInputRules[:i], curTSInputRules[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
curIPT[chain] = curTSInputRules
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
curIPT := n.ipt4
|
||||
if addr.Is6() {
|
||||
curIPT = n.ipt6
|
||||
}
|
||||
newRule := fmt.Sprintf("-i lo -s %s -j ACCEPT", addr.String())
|
||||
|
||||
return insertRule(n, curIPT, "filter/ts-input", newRule)
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddBase(tunname string) error {
|
||||
if err := n.AddBase4(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.HasIPV6() {
|
||||
if err := n.AddBase6(tunname); err != nil {
|
||||
return err
|
||||
func (n *fakeNetfilter) Insert(table, chain string, pos int, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if pos > len(rules)+1 {
|
||||
n.t.Errorf("bad position %d in %s", pos, k)
|
||||
return errExec
|
||||
}
|
||||
rules = append(rules, "")
|
||||
copy(rules[pos:], rules[pos-1:])
|
||||
rules[pos-1] = strings.Join(args, " ")
|
||||
n.n[k] = rules
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return errExec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddBase4(tunname string) error {
|
||||
curIPT := n.ipt4
|
||||
newRules := []struct{ chain, rule string }{
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j RETURN", tunname, tsaddr.ChromeOSVMRange().String())},
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j DROP", tunname, tsaddr.CGNATRange().String())},
|
||||
{"filter/ts-forward", fmt.Sprintf("-i %s -j MARK --set-mark %s/%s", tunname, linuxfw.TailscaleSubnetRouteMark, linuxfw.TailscaleFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-m mark --mark %s/%s -j ACCEPT", linuxfw.TailscaleSubnetRouteMark, linuxfw.TailscaleFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-o %s -s %s -j DROP", tunname, tsaddr.CGNATRange().String())},
|
||||
{"filter/ts-forward", fmt.Sprintf("-o %s -j ACCEPT", tunname)},
|
||||
}
|
||||
for _, rule := range newRules {
|
||||
if err := appendRule(n, curIPT, rule.chain, rule.rule); err != nil {
|
||||
return fmt.Errorf("add rule %q to chain %q: %w", rule.rule, rule.chain, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func (n *fakeNetfilter) Append(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
return n.Insert(table, chain, len(n.n[k])+1, args...)
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddBase6(tunname string) error {
|
||||
curIPT := n.ipt6
|
||||
newRules := []struct{ chain, rule string }{
|
||||
{"filter/ts-forward", fmt.Sprintf("-i %s -j MARK --set-mark %s/%s", tunname, linuxfw.TailscaleSubnetRouteMark, linuxfw.TailscaleFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-m mark --mark %s/%s -j ACCEPT", linuxfw.TailscaleSubnetRouteMark, linuxfw.TailscaleFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-o %s -j ACCEPT", tunname)},
|
||||
}
|
||||
for _, rule := range newRules {
|
||||
if err := appendRule(n, curIPT, rule.chain, rule.rule); err != nil {
|
||||
return fmt.Errorf("add rule %q to chain %q: %w", rule.rule, rule.chain, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
curIPT := n.ipt4
|
||||
if addr.Is6() {
|
||||
curIPT = n.ipt6
|
||||
}
|
||||
|
||||
delRule := fmt.Sprintf("-i lo -s %s -j ACCEPT", addr.String())
|
||||
|
||||
return deleteRule(n, curIPT, "filter/ts-input", delRule)
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddHooks() error {
|
||||
newRules := []struct{ chain, rule string }{
|
||||
{"filter/INPUT", "-j ts-input"},
|
||||
{"filter/FORWARD", "-j ts-forward"},
|
||||
{"nat/POSTROUTING", "-j ts-postrouting"},
|
||||
}
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
for _, r := range newRules {
|
||||
if err := insertRule(n, ipt, r.chain, r.rule); err != nil {
|
||||
return err
|
||||
func (n *fakeNetfilter) Exists(table, chain string, args ...string) (bool, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for _, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return false, errExec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelHooks(logf logger.Logf) error {
|
||||
delRules := []struct{ chain, rule string }{
|
||||
{"filter/INPUT", "-j ts-input"},
|
||||
{"filter/FORWARD", "-j ts-forward"},
|
||||
{"nat/POSTROUTING", "-j ts-postrouting"},
|
||||
}
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
for _, r := range delRules {
|
||||
if err := deleteRule(n, ipt, r.chain, r.rule); err != nil {
|
||||
return err
|
||||
func (n *fakeNetfilter) Delete(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for i, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
rules = append(rules[:i], rules[i+1:]...)
|
||||
n.n[k] = rules
|
||||
return nil
|
||||
}
|
||||
}
|
||||
n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
|
||||
return errExec
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return errExec
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeNetfilter) ClearChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
} else {
|
||||
n.t.Logf("note: ClearChain: unknown table/chain %s", k)
|
||||
return errors.New("exitcode:1")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeNetfilter) NewChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.t.Errorf("table/chain %s already exists", k)
|
||||
return errExec
|
||||
}
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddChains() error {
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
for _, chain := range []string{"filter/ts-input", "filter/ts-forward", "nat/ts-postrouting"} {
|
||||
ipt[chain] = nil
|
||||
func (n *fakeNetfilter) DeleteChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if len(rules) != 0 {
|
||||
n.t.Errorf("%s is not empty", k)
|
||||
return errExec
|
||||
}
|
||||
delete(n.n, k)
|
||||
return nil
|
||||
} else {
|
||||
n.t.Errorf("%s does not exist", k)
|
||||
return errExec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelChains() error {
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
for chain := range ipt {
|
||||
if strings.HasPrefix(chain, "filter/ts-") || strings.HasPrefix(chain, "nat/ts-") {
|
||||
delete(ipt, chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelBase() error {
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
for _, chain := range []string{"filter/ts-input", "filter/ts-forward", "nat/ts-postrouting"} {
|
||||
ipt[chain] = nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddSNATRule() error {
|
||||
newRule := fmt.Sprintf("-m mark --mark %s/%s -j MASQUERADE", linuxfw.TailscaleSubnetRouteMark, linuxfw.TailscaleFwmarkMask)
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
if err := appendRule(n, ipt, "nat/ts-postrouting", newRule); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelSNATRule() error {
|
||||
delRule := fmt.Sprintf("-m mark --mark %s/%s -j MASQUERADE", linuxfw.TailscaleSubnetRouteMark, linuxfw.TailscaleFwmarkMask)
|
||||
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||
if err := deleteRule(n, ipt, "nat/ts-postrouting", delRule); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) HasIPV6() bool { return true }
|
||||
func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true }
|
||||
|
||||
// fakeOS implements commandRunner and provides v4 and v6
|
||||
// netfilterRunners, but captures changes without touching the OS.
|
||||
type fakeOS struct {
|
||||
t *testing.T
|
||||
up bool
|
||||
ips []string
|
||||
routes []string
|
||||
rules []string
|
||||
//This test tests on the router level, so we will not bother
|
||||
//with using iptables or nftables, chose the simpler one.
|
||||
nfr netfilterRunner
|
||||
t *testing.T
|
||||
up bool
|
||||
ips []string
|
||||
routes []string
|
||||
rules []string
|
||||
netfilter4 *fakeNetfilter
|
||||
netfilter6 *fakeNetfilter
|
||||
}
|
||||
|
||||
func NewFakeOS(t *testing.T) *fakeOS {
|
||||
return &fakeOS{
|
||||
t: t,
|
||||
nfr: newIPTablesRunner(t),
|
||||
t: t,
|
||||
netfilter4: newNetfilter(t),
|
||||
netfilter6: newNetfilter(t),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -635,23 +516,23 @@ func (o *fakeOS) String() string {
|
|||
}
|
||||
|
||||
var chains []string
|
||||
for chain := range o.nfr.(*fakeIPTablesRunner).ipt4 {
|
||||
for chain := range o.netfilter4.n {
|
||||
chains = append(chains, chain)
|
||||
}
|
||||
sort.Strings(chains)
|
||||
for _, chain := range chains {
|
||||
for _, rule := range o.nfr.(*fakeIPTablesRunner).ipt4[chain] {
|
||||
for _, rule := range o.netfilter4.n[chain] {
|
||||
fmt.Fprintf(&b, "v4/%s %s\n", chain, rule)
|
||||
}
|
||||
}
|
||||
|
||||
chains = nil
|
||||
for chain := range o.nfr.(*fakeIPTablesRunner).ipt6 {
|
||||
for chain := range o.netfilter6.n {
|
||||
chains = append(chains, chain)
|
||||
}
|
||||
sort.Strings(chains)
|
||||
for _, chain := range chains {
|
||||
for _, rule := range o.nfr.(*fakeIPTablesRunner).ipt6[chain] {
|
||||
for _, rule := range o.netfilter6.n[chain] {
|
||||
fmt.Fprintf(&b, "v6/%s %s\n", chain, rule)
|
||||
}
|
||||
}
|
||||
|
@ -925,7 +806,7 @@ func TestDebugListRules(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCheckIPRuleSupportsV6(t *testing.T) {
|
||||
err := linuxfw.CheckIPRuleSupportsV6(t.Logf)
|
||||
err := checkIPRuleSupportsV6(t.Logf)
|
||||
if err != nil && os.Getuid() != 0 {
|
||||
t.Skipf("skipping, error when not root: %v", err)
|
||||
}
|
||||
|
|
|
@ -96,6 +96,9 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
|
|||
DiscoKey: peer.DiscoKey,
|
||||
})
|
||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
if peer.KeepAlive {
|
||||
cpeer.PersistentKeepalive = 25 // seconds
|
||||
}
|
||||
|
||||
didExitNodeWarn := false
|
||||
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer
|
||||
|
|
|
@ -320,3 +320,4 @@ vimba
|
|||
wahoo
|
||||
zebra
|
||||
coelacanth
|
||||
cherimoya
|
||||
|
|
|
@ -544,4 +544,4 @@ shrimp
|
|||
prawn
|
||||
lobster
|
||||
chipmunk
|
||||
tails
|
||||
kite
|
||||
|
|
Loading…
Reference in New Issue