Compare commits
79 Commits
main
...
bradfitz/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc78993f8d | ||
|
|
31a2724245 | ||
|
|
b0d9b9c7b7 | ||
|
|
674888e564 | ||
|
|
75631c5d9d | ||
|
|
bfaf33d767 | ||
|
|
1aac5cddd8 | ||
|
|
36073dd8e6 | ||
|
|
0067f45f67 | ||
|
|
9edf7ad1a9 | ||
|
|
595f18d875 | ||
|
|
edb072e548 | ||
|
|
b9745a5375 | ||
|
|
c781951fdd | ||
|
|
e4fdbc3ff1 | ||
|
|
28a010dd4c | ||
|
|
23b3ebeaa9 | ||
|
|
f2e7d13b7c | ||
|
|
718e30c4b9 | ||
|
|
9abc1629fa | ||
|
|
3e653d4e53 | ||
|
|
188deab708 | ||
|
|
3d08fb23e7 | ||
|
|
e90284980b | ||
|
|
4bea980e15 | ||
|
|
a824172cdb | ||
|
|
bca75cc9e4 | ||
|
|
a3149830f2 | ||
|
|
2f20dbd71e | ||
|
|
cc26a65cbe | ||
|
|
36c23ddc9a | ||
|
|
6279f23266 | ||
|
|
79b8c00c7a | ||
|
|
d701e47840 | ||
|
|
c6c16fdc96 | ||
|
|
e2d7f94f46 | ||
|
|
f331f31a58 | ||
|
|
c299606f3c | ||
|
|
5d1a69822e | ||
|
|
d31dceaffd | ||
|
|
a4948db5a3 | ||
|
|
c85d84a099 | ||
|
|
454126075d | ||
|
|
77d777259c | ||
|
|
53b74f1cb7 | ||
|
|
d6f275fb65 | ||
|
|
7d9bbf51cd | ||
|
|
0f7aecf312 | ||
|
|
5603a35aba | ||
|
|
65340c02fc | ||
|
|
dcc759ff35 | ||
|
|
182dad243b | ||
|
|
901f12ed39 | ||
|
|
bf1a9cd7dc | ||
|
|
b50bfa231b | ||
|
|
202014a534 | ||
|
|
253bf1b360 | ||
|
|
501fd9f508 | ||
|
|
e47cc6bee2 | ||
|
|
b132a59f74 | ||
|
|
26985cf12a | ||
|
|
5d793c0003 | ||
|
|
9bf8966c26 | ||
|
|
d086f952b9 | ||
|
|
ccd498f266 | ||
|
|
51491012ec | ||
|
|
2a4a7f2fc7 | ||
|
|
414e1e1238 | ||
|
|
0dac8dfb94 | ||
|
|
55587d3156 | ||
|
|
5b4d3207b0 | ||
|
|
2348da8980 | ||
|
|
f86f0f793a | ||
|
|
ce2d6d4665 | ||
|
|
aa0ec3e4a5 | ||
|
|
cdcf8d51cf | ||
|
|
30d682899b | ||
|
|
515d9689d5 | ||
|
|
45a2d139a1 |
20
Makefile
20
Makefile
@@ -24,7 +24,25 @@ updatedeps: ## Update depaware deps
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
|
||||
depaware: ## Run depaware checks
|
||||
MIN_OMITS ?= ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_netstack,ts_omit_nftables,ts_omit_ssh,ts_omit_tka,ts_omit_webclient,ts_omit_h2c
|
||||
|
||||
min:
|
||||
./tool/go build -o $$HOME/bin/tailscaled.min -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscaled
|
||||
GOOS=linux ./tool/go build -o $$HOME/bin/tailscaled.minlinux -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscaled
|
||||
GOOS=linux ./tool/go build -o $$HOME/bin/tailscale.minlinux -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscale
|
||||
ls -l $$HOME/bin/tailscaled.min{,linux}
|
||||
min-amd64:
|
||||
./tool/go build -o $$HOME/bin/tailscaled.min -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscaled
|
||||
GOOS=linux GOARCH=amd64 ./tool/go build -o $$HOME/bin/tailscaled.min.linux-amd64 -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscaled
|
||||
GOOS=linux GOARCH=amd64 ./tool/go build -o $$HOME/bin/tailscale.min.linux-amd64 -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscale
|
||||
ls -l $$HOME/bin/tailscale*.min.linux-amd64
|
||||
|
||||
updatemindeps: min
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --file=depaware-minlinux.txt --goos=linux,darwin --tags=${MIN_OMITS} --update \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale
|
||||
|
||||
depaware: ## Run depaware checksq
|
||||
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
|
||||
# it finds in its $$PATH is the right one.
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check \
|
||||
|
||||
@@ -6,7 +6,6 @@ package apitype
|
||||
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
// LocalAPIHost is the Host header value used by the LocalAPI.
|
||||
@@ -73,6 +72,4 @@ type DNSOSConfig struct {
|
||||
type DNSQueryResponse struct {
|
||||
// Bytes is the raw DNS response bytes.
|
||||
Bytes []byte
|
||||
// Resolvers is the list of resolvers that the forwarder deemed able to resolve the query.
|
||||
Resolvers []*dnstype.Resolver
|
||||
}
|
||||
|
||||
@@ -29,18 +29,13 @@ import (
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
// defaultLocalClient is the default LocalClient when using the legacy
|
||||
@@ -145,9 +140,6 @@ func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error)
|
||||
func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
|
||||
res, err := lc.DoLocalRequest(req)
|
||||
if err == nil {
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != envknob.IPCVersion() && onVersionMismatch != nil {
|
||||
onVersionMismatch(envknob.IPCVersion(), server)
|
||||
}
|
||||
if res.StatusCode == 403 {
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
|
||||
@@ -826,33 +818,6 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
|
||||
return decodeJSON[*ipn.Prefs](body)
|
||||
}
|
||||
|
||||
// GetEffectivePolicy returns the effective policy for the specified scope.
|
||||
func (lc *LocalClient) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||
scopeID, err := scope.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*setting.Snapshot](body)
|
||||
}
|
||||
|
||||
// ReloadEffectivePolicy reloads the effective policy for the specified scope
|
||||
// by reading and merging policy settings from all applicable policy sources.
|
||||
func (lc *LocalClient) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||
scopeID, err := scope.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*setting.Snapshot](body)
|
||||
}
|
||||
|
||||
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
||||
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
||||
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
||||
@@ -867,21 +832,6 @@ func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig
|
||||
return &osCfg, nil
|
||||
}
|
||||
|
||||
// QueryDNS executes a DNS query for a name (`google.com.`) and query type (`CNAME`).
|
||||
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
|
||||
// (often just one, but can be more if we raced multiple resolvers).
|
||||
func (lc *LocalClient) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
|
||||
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var res apitype.DNSQueryResponse
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid query response: %w", err)
|
||||
}
|
||||
return res.Bytes, res.Resolvers, nil
|
||||
}
|
||||
|
||||
// StartLoginInteractive starts an interactive login.
|
||||
func (lc *LocalClient) StartLoginInteractive(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
|
||||
@@ -901,28 +851,6 @@ func (lc *LocalClient) Logout(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDNS adds a DNS TXT record for the given domain name, containing
|
||||
// the provided TXT value. The intended use case is answering
|
||||
// LetsEncrypt/ACME dns-01 challenges.
|
||||
//
|
||||
// The control plane will only permit SetDNS requests with very
|
||||
// specific names and values. The name should be
|
||||
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
||||
// clients cache the certs from LetsEncrypt (or whichever CA is
|
||||
// providing them) and only request new ones as needed; the control plane
|
||||
// rate limits SetDNS requests.
|
||||
//
|
||||
// This is a low-level interface; it's expected that most Tailscale
|
||||
// users use a higher level interface to getting/using TLS
|
||||
// certificates.
|
||||
func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
|
||||
v := url.Values{}
|
||||
v.Set("name", name)
|
||||
v.Set("value", value)
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// DialTCP connects to the host's port via Tailscale.
|
||||
//
|
||||
// The host may be a base DNS name (resolved from the netmap inside
|
||||
@@ -1147,183 +1075,6 @@ func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg
|
||||
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
|
||||
}
|
||||
|
||||
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
||||
func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockInit initializes the tailnet key authority.
|
||||
//
|
||||
// TODO(tom): Plumb through disablement secrets.
|
||||
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
|
||||
var b bytes.Buffer
|
||||
type initRequest struct {
|
||||
Keys []tka.Key
|
||||
DisablementValues [][]byte
|
||||
SupportDisablement []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||
encodedPrivate, err := tkaKey.MarshalText()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
type wrapRequest struct {
|
||||
TSKey string
|
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
}
|
||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||
var b bytes.Buffer
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
RemoveKeys []tka.Key
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
|
||||
// rotationPublic, if specified, must be an ed25519 public key.
|
||||
func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
|
||||
var b bytes.Buffer
|
||||
type signRequest struct {
|
||||
NodeKey key.NodePublic
|
||||
RotationPublic []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||
func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
||||
}
|
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
v := url.Values{}
|
||||
v.Set("limit", fmt.Sprint(maxEntries))
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
|
||||
}
|
||||
|
||||
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
||||
func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||
// This endpoint expects an empty JSON stanza as the payload.
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
||||
func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
|
||||
vr := struct {
|
||||
Keys []tkatype.KeyID
|
||||
ForkFrom string
|
||||
}{removeKeys, forkFrom.String()}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
||||
func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
||||
func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -190,10 +190,8 @@ change in the future.
|
||||
logoutCmd,
|
||||
switchCmd,
|
||||
configureCmd,
|
||||
syspolicyCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
dnsCmd,
|
||||
statusCmd,
|
||||
metricsCmd,
|
||||
pingCmd,
|
||||
@@ -206,7 +204,6 @@ change in the future.
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
netlockCmd,
|
||||
licensesCmd,
|
||||
exitNodeCmd(),
|
||||
updateCmd,
|
||||
|
||||
@@ -830,7 +830,6 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
log.Printf("tshttpproxy.ProxyFromEnvironment = (%v, %v)", proxy, err)
|
||||
}
|
||||
machinePrivate := key.NewMachine()
|
||||
var dialer net.Dialer
|
||||
|
||||
var keys struct {
|
||||
PublicKey key.MachinePublic
|
||||
@@ -858,19 +857,6 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
log.Printf("got public key: %v", keys.PublicKey)
|
||||
}
|
||||
|
||||
dialFunc := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
log.Printf("Dial(%q, %q) ...", network, address)
|
||||
c, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
// skip logging context cancellation errors
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
log.Printf("Dial(%q, %q) = %v", network, address, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Dial(%q, %q) = %v / %v", network, address, c.LocalAddr(), c.RemoteAddr())
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
var logf logger.Logf
|
||||
if ts2021Args.verbose {
|
||||
logf = log.Printf
|
||||
@@ -888,7 +874,6 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
MachineKey: machinePrivate,
|
||||
ControlKey: keys.PublicKey,
|
||||
ProtocolVersion: uint16(ts2021Args.version),
|
||||
Dialer: dialFunc,
|
||||
Logf: logf,
|
||||
NetMon: netMon,
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
func runDNSQuery(ctx context.Context, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
name := args[0]
|
||||
queryType := "A"
|
||||
if len(args) >= 2 {
|
||||
queryType = args[1]
|
||||
}
|
||||
fmt.Printf("DNS query for %q (%s) using internal resolver:\n", name, queryType)
|
||||
fmt.Println()
|
||||
bytes, resolvers, err := localClient.QueryDNS(ctx, name, queryType)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to query DNS: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(resolvers) == 1 {
|
||||
fmt.Printf("Forwarding to resolver: %v\n", makeResolverString(*resolvers[0]))
|
||||
} else {
|
||||
fmt.Println("Multiple resolvers available:")
|
||||
for _, r := range resolvers {
|
||||
fmt.Printf(" - %v\n", makeResolverString(*r))
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
var p dnsmessage.Parser
|
||||
header, err := p.Start(bytes)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to parse DNS response: %v\n", err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Response code: %v\n", header.RCode.String())
|
||||
fmt.Println()
|
||||
p.SkipAllQuestions()
|
||||
if header.RCode != dnsmessage.RCodeSuccess {
|
||||
fmt.Println("No answers were returned.")
|
||||
return nil
|
||||
}
|
||||
answers, err := p.AllAnswers()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to parse DNS answers: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if len(answers) == 0 {
|
||||
fmt.Println(" (no answers found)")
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "Name\tTTL\tClass\tType\tBody")
|
||||
fmt.Fprintln(w, "----\t---\t-----\t----\t----")
|
||||
for _, a := range answers {
|
||||
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", a.Header.Name.String(), a.Header.TTL, a.Header.Class.String(), a.Header.Type.String(), makeAnswerBody(a))
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeAnswerBody returns a string with the DNS answer body in a human-readable format.
|
||||
func makeAnswerBody(a dnsmessage.Resource) string {
|
||||
switch a.Header.Type {
|
||||
case dnsmessage.TypeA:
|
||||
return makeABody(a.Body)
|
||||
case dnsmessage.TypeAAAA:
|
||||
return makeAAAABody(a.Body)
|
||||
case dnsmessage.TypeCNAME:
|
||||
return makeCNAMEBody(a.Body)
|
||||
case dnsmessage.TypeMX:
|
||||
return makeMXBody(a.Body)
|
||||
case dnsmessage.TypeNS:
|
||||
return makeNSBody(a.Body)
|
||||
case dnsmessage.TypeOPT:
|
||||
return makeOPTBody(a.Body)
|
||||
case dnsmessage.TypePTR:
|
||||
return makePTRBody(a.Body)
|
||||
case dnsmessage.TypeSRV:
|
||||
return makeSRVBody(a.Body)
|
||||
case dnsmessage.TypeTXT:
|
||||
return makeTXTBody(a.Body)
|
||||
default:
|
||||
return a.Body.GoString()
|
||||
}
|
||||
}
|
||||
|
||||
func makeABody(a dnsmessage.ResourceBody) string {
|
||||
if a, ok := a.(*dnsmessage.AResource); ok {
|
||||
return netip.AddrFrom4(a.A).String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeAAAABody(aaaa dnsmessage.ResourceBody) string {
|
||||
if a, ok := aaaa.(*dnsmessage.AAAAResource); ok {
|
||||
return netip.AddrFrom16(a.AAAA).String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeCNAMEBody(cname dnsmessage.ResourceBody) string {
|
||||
if c, ok := cname.(*dnsmessage.CNAMEResource); ok {
|
||||
return c.CNAME.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeMXBody(mx dnsmessage.ResourceBody) string {
|
||||
if m, ok := mx.(*dnsmessage.MXResource); ok {
|
||||
return fmt.Sprintf("%s (Priority=%d)", m.MX, m.Pref)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeNSBody(ns dnsmessage.ResourceBody) string {
|
||||
if n, ok := ns.(*dnsmessage.NSResource); ok {
|
||||
return n.NS.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeOPTBody(opt dnsmessage.ResourceBody) string {
|
||||
if o, ok := opt.(*dnsmessage.OPTResource); ok {
|
||||
return o.GoString()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makePTRBody(ptr dnsmessage.ResourceBody) string {
|
||||
if p, ok := ptr.(*dnsmessage.PTRResource); ok {
|
||||
return p.PTR.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeSRVBody(srv dnsmessage.ResourceBody) string {
|
||||
if s, ok := srv.(*dnsmessage.SRVResource); ok {
|
||||
return fmt.Sprintf("Target=%s, Port=%d, Priority=%d, Weight=%d", s.Target.String(), s.Port, s.Priority, s.Weight)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeTXTBody(txt dnsmessage.ResourceBody) string {
|
||||
if t, ok := txt.(*dnsmessage.TXTResource); ok {
|
||||
return fmt.Sprintf("%q", t.TXT)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func makeResolverString(r dnstype.Resolver) string {
|
||||
if len(r.BootstrapResolution) > 0 {
|
||||
return fmt.Sprintf("%s (bootstrap: %v)", r.Addr, r.BootstrapResolution)
|
||||
}
|
||||
return fmt.Sprintf("%s", r.Addr)
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
// dnsStatusArgs are the arguments for the "dns status" subcommand.
|
||||
var dnsStatusArgs struct {
|
||||
all bool
|
||||
}
|
||||
|
||||
func runDNSStatus(ctx context.Context, args []string) error {
|
||||
all := dnsStatusArgs.all
|
||||
s, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefs, err := localClient.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enabledStr := "disabled.\n\n(Run 'tailscale set --accept-dns=true' to start sending DNS queries to the Tailscale DNS resolver)"
|
||||
if prefs.CorpDNS {
|
||||
enabledStr = "enabled.\n\nTailscale is configured to handle DNS queries on this device.\nRun 'tailscale set --accept-dns=false' to revert to your system default DNS resolver."
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("=== 'Use Tailscale DNS' status ===")
|
||||
fmt.Print("\n")
|
||||
fmt.Printf("Tailscale DNS: %s\n", enabledStr)
|
||||
fmt.Print("\n")
|
||||
fmt.Println("=== MagicDNS configuration ===")
|
||||
fmt.Print("\n")
|
||||
fmt.Println("This is the DNS configuration provided by the coordination server to this device.")
|
||||
fmt.Print("\n")
|
||||
if s.CurrentTailnet == nil {
|
||||
fmt.Println("No tailnet information available; make sure you're logged in to a tailnet.")
|
||||
return nil
|
||||
} else if s.CurrentTailnet.MagicDNSEnabled {
|
||||
fmt.Printf("MagicDNS: enabled tailnet-wide (suffix = %s)", s.CurrentTailnet.MagicDNSSuffix)
|
||||
fmt.Print("\n\n")
|
||||
fmt.Printf("Other devices in your tailnet can reach this device at %s\n", s.Self.DNSName)
|
||||
} else {
|
||||
fmt.Printf("MagicDNS: disabled tailnet-wide.\n")
|
||||
}
|
||||
fmt.Print("\n")
|
||||
|
||||
netMap, err := fetchNetMap()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to fetch network map: %v\n", err)
|
||||
return err
|
||||
}
|
||||
dnsConfig := netMap.DNS
|
||||
fmt.Println("Resolvers (in preference order):")
|
||||
if len(dnsConfig.Resolvers) == 0 {
|
||||
fmt.Println(" (no resolvers configured, system default will be used: see 'System DNS configuration' below)")
|
||||
}
|
||||
for _, r := range dnsConfig.Resolvers {
|
||||
fmt.Printf(" - %v", r.Addr)
|
||||
if r.BootstrapResolution != nil {
|
||||
fmt.Printf(" (bootstrap: %v)", r.BootstrapResolution)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Split DNS Routes:")
|
||||
if len(dnsConfig.Routes) == 0 {
|
||||
fmt.Println(" (no routes configured: split DNS disabled)")
|
||||
}
|
||||
for _, k := range slices.Sorted(maps.Keys(dnsConfig.Routes)) {
|
||||
v := dnsConfig.Routes[k]
|
||||
for _, r := range v {
|
||||
fmt.Printf(" - %-30s -> %v", k, r.Addr)
|
||||
if r.BootstrapResolution != nil {
|
||||
fmt.Printf(" (bootstrap: %v)", r.BootstrapResolution)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
}
|
||||
fmt.Print("\n")
|
||||
if all {
|
||||
fmt.Println("Fallback Resolvers:")
|
||||
if len(dnsConfig.FallbackResolvers) == 0 {
|
||||
fmt.Println(" (no fallback resolvers configured)")
|
||||
}
|
||||
for i, r := range dnsConfig.FallbackResolvers {
|
||||
fmt.Printf(" %d: %v\n", i, r)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
fmt.Println("Search Domains:")
|
||||
if len(dnsConfig.Domains) == 0 {
|
||||
fmt.Println(" (no search domains configured)")
|
||||
}
|
||||
domains := dnsConfig.Domains
|
||||
slices.Sort(domains)
|
||||
for _, r := range domains {
|
||||
fmt.Printf(" - %v\n", r)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
if all {
|
||||
fmt.Println("Nameservers IP Addresses:")
|
||||
if len(dnsConfig.Nameservers) == 0 {
|
||||
fmt.Println(" (none were provided)")
|
||||
}
|
||||
for _, r := range dnsConfig.Nameservers {
|
||||
fmt.Printf(" - %v\n", r)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Certificate Domains:")
|
||||
if len(dnsConfig.CertDomains) == 0 {
|
||||
fmt.Println(" (no certificate domains are configured)")
|
||||
}
|
||||
for _, r := range dnsConfig.CertDomains {
|
||||
fmt.Printf(" - %v\n", r)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Additional DNS Records:")
|
||||
if len(dnsConfig.ExtraRecords) == 0 {
|
||||
fmt.Println(" (no extra records are configured)")
|
||||
}
|
||||
for _, er := range dnsConfig.ExtraRecords {
|
||||
if er.Type == "" {
|
||||
fmt.Printf(" - %-50s -> %v\n", er.Name, er.Value)
|
||||
} else {
|
||||
fmt.Printf(" - [%s] %-50s -> %v\n", er.Type, er.Name, er.Value)
|
||||
}
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Filtered suffixes when forwarding DNS queries as an exit node:")
|
||||
if len(dnsConfig.ExitNodeFilteredSet) == 0 {
|
||||
fmt.Println(" (no suffixes are filtered)")
|
||||
}
|
||||
for _, s := range dnsConfig.ExitNodeFilteredSet {
|
||||
fmt.Printf(" - %s\n", s)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
fmt.Println("=== System DNS configuration ===")
|
||||
fmt.Print("\n")
|
||||
fmt.Println("This is the DNS configuration that Tailscale believes your operating system is using.\nTailscale may use this configuration if 'Override Local DNS' is disabled in the admin console,\nor if no resolvers are provided by the coordination server.")
|
||||
fmt.Print("\n")
|
||||
osCfg, err := localClient.GetDNSOSConfig(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not supported") {
|
||||
// avoids showing the HTTP error code which would be odd here
|
||||
fmt.Println(" (reading the system DNS configuration is not supported on this platform)")
|
||||
} else {
|
||||
fmt.Printf(" (failed to read system DNS configuration: %v)\n", err)
|
||||
}
|
||||
} else if osCfg == nil {
|
||||
fmt.Println(" (no OS DNS configuration available)")
|
||||
} else {
|
||||
fmt.Println("Nameservers:")
|
||||
if len(osCfg.Nameservers) == 0 {
|
||||
fmt.Println(" (no nameservers found, DNS queries might fail\nunless the coordination server is providing a nameserver)")
|
||||
}
|
||||
for _, ns := range osCfg.Nameservers {
|
||||
fmt.Printf(" - %v\n", ns)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Search domains:")
|
||||
if len(osCfg.SearchDomains) == 0 {
|
||||
fmt.Println(" (no search domains found)")
|
||||
}
|
||||
for _, sd := range osCfg.SearchDomains {
|
||||
fmt.Printf(" - %v\n", sd)
|
||||
}
|
||||
if all {
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Match domains:")
|
||||
if len(osCfg.MatchDomains) == 0 {
|
||||
fmt.Println(" (no match domains found)")
|
||||
}
|
||||
for _, md := range osCfg.MatchDomains {
|
||||
fmt.Printf(" - %v\n", md)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println("[this is a preliminary version of this command; the output format may change in the future]")
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchNetMap() (netMap *netmap.NetworkMap, err error) {
|
||||
w, err := localClient.WatchIPNBus(context.Background(), ipn.NotifyInitialNetMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer w.Close()
|
||||
notify, err := w.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if notify.NetMap == nil {
|
||||
return nil, fmt.Errorf("no network map yet available, please try again later")
|
||||
}
|
||||
return notify.NetMap, nil
|
||||
}
|
||||
|
||||
func dnsStatusLongHelp() string {
|
||||
return `The 'tailscale dns status' subcommand prints the current DNS status and configuration, including:
|
||||
|
||||
- Whether the built-in DNS forwarder is enabled.
|
||||
- The MagicDNS configuration provided by the coordination server.
|
||||
- Details on which resolver(s) Tailscale believes the system is using by default.
|
||||
|
||||
The --all flag can be used to output advanced debugging information, including fallback resolvers, nameservers, certificate domains, extra records, and the exit node filtered set.
|
||||
|
||||
=== Contents of the MagicDNS configuration ===
|
||||
|
||||
The MagicDNS configuration is provided by the coordination server to the client and includes the following components:
|
||||
|
||||
- MagicDNS enablement status: Indicates whether MagicDNS is enabled across the entire tailnet.
|
||||
|
||||
- MagicDNS Suffix: The DNS suffix used for devices within your tailnet.
|
||||
|
||||
- DNS Name: The DNS name that other devices in the tailnet can use to reach this device.
|
||||
|
||||
- Resolvers: The preferred DNS resolver(s) to be used for resolving queries, in order of preference. If no resolvers are listed here, the system defaults are used.
|
||||
|
||||
- Split DNS Routes: Custom DNS resolvers may be used to resolve hostnames in specific domains, this is also known as a 'Split DNS' configuration. The mapping of domains to their respective resolvers is provided here.
|
||||
|
||||
- Certificate Domains: The DNS names for which the coordination server will assist in provisioning TLS certificates.
|
||||
|
||||
- Extra Records: Additional DNS records that the coordination server might provide to the internal DNS resolver.
|
||||
|
||||
- Exit Node Filtered Set: DNS suffixes that the node, when acting as an exit node DNS proxy, will not answer.
|
||||
|
||||
For more information about the DNS functionality built into Tailscale, refer to https://tailscale.com/kb/1054/dns.`
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
|
||||
var dnsCmd = &ffcli.Command{
|
||||
Name: "dns",
|
||||
ShortHelp: "Diagnose the internal DNS forwarder",
|
||||
LongHelp: dnsCmdLongHelp(),
|
||||
ShortUsage: "tailscale dns <subcommand> [flags]",
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
ShortUsage: "tailscale dns status [--all]",
|
||||
Exec: runDNSStatus,
|
||||
ShortHelp: "Prints the current DNS status and configuration",
|
||||
LongHelp: dnsStatusLongHelp(),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&dnsStatusArgs.all, "all", false, "outputs advanced debugging information (fallback resolvers, nameservers, cert domains, extra records, and exit node filtered set)")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "query",
|
||||
ShortUsage: "tailscale dns query <name> [a|aaaa|cname|mx|ns|opt|ptr|srv|txt]",
|
||||
Exec: runDNSQuery,
|
||||
ShortHelp: "Perform a DNS query",
|
||||
LongHelp: "The 'tailscale dns query' subcommand performs a DNS query for the specified name using the internal DNS forwarder (100.100.100.100).\n\nIt also provides information about the resolver(s) used to resolve the query.",
|
||||
},
|
||||
|
||||
// TODO: implement `tailscale log` here
|
||||
|
||||
// The above work is tracked in https://github.com/tailscale/tailscale/issues/13326
|
||||
},
|
||||
}
|
||||
|
||||
func dnsCmdLongHelp() string {
|
||||
return `The 'tailscale dns' subcommand provides tools for diagnosing the internal DNS forwarder (100.100.100.100).
|
||||
|
||||
For more information about the DNS functionality built into Tailscale, refer to https://tailscale.com/kb/1054/dns.`
|
||||
}
|
||||
@@ -59,9 +59,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
defer pm.Close()
|
||||
|
||||
c := &netcheck.Client{
|
||||
NetMon: netMon,
|
||||
PortMapper: pm,
|
||||
UseDNSCache: false, // always resolve, don't cache
|
||||
NetMon: netMon,
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tka
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
|
||||
@@ -12,24 +12,21 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"golang.org/x/net/idna"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "tailscale status [--active] [--web] [--json]",
|
||||
ShortUsage: "tailscale status [--active] [--json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -50,7 +47,6 @@ https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
||||
@@ -62,7 +58,6 @@ https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
|
||||
|
||||
var statusArgs struct {
|
||||
json bool // JSON output mode
|
||||
web bool // run webserver
|
||||
listen string // in web mode, webserver address to listen on, empty means auto
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
@@ -97,38 +92,6 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
printf("%s", j)
|
||||
return nil
|
||||
}
|
||||
if statusArgs.web {
|
||||
ln, err := net.Listen("tcp", statusArgs.listen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statusURL := netmon.HTTPOfListener(ln)
|
||||
printf("Serving Tailscale status at %v ...\n", statusURL)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
}()
|
||||
if statusArgs.browser {
|
||||
go webbrowser.Open(statusURL)
|
||||
}
|
||||
err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.RequestURI != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st.WriteHTML(w)
|
||||
}))
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
printHealth := func() {
|
||||
printf("# Health check:\n")
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
var syspolicyArgs struct {
|
||||
json bool // JSON output mode
|
||||
}
|
||||
|
||||
var syspolicyCmd = &ffcli.Command{
|
||||
Name: "syspolicy",
|
||||
ShortHelp: "Diagnose the MDM and system policy configuration",
|
||||
LongHelp: "The 'tailscale syspolicy' command provides tools for diagnosing the MDM and system policy configuration.",
|
||||
ShortUsage: "tailscale syspolicy <subcommand>",
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
ShortUsage: "tailscale syspolicy list",
|
||||
Exec: runSysPolicyList,
|
||||
ShortHelp: "Prints effective policy settings",
|
||||
LongHelp: "The 'tailscale syspolicy list' subcommand displays the effective policy settings and their sources (e.g., MDM or environment variables).",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("syspolicy list")
|
||||
fs.BoolVar(&syspolicyArgs.json, "json", false, "output in JSON format")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "reload",
|
||||
ShortUsage: "tailscale syspolicy reload",
|
||||
Exec: runSysPolicyReload,
|
||||
ShortHelp: "Forces a reload of policy settings, even if no changes are detected, and prints the result",
|
||||
LongHelp: "The 'tailscale syspolicy reload' subcommand forces a reload of policy settings, even if no changes are detected, and prints the result.",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("syspolicy reload")
|
||||
fs.BoolVar(&syspolicyArgs.json, "json", false, "output in JSON format")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runSysPolicyList(ctx context.Context, args []string) error {
|
||||
policy, err := localClient.GetEffectivePolicy(ctx, setting.DefaultScope())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printPolicySettings(policy)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func runSysPolicyReload(ctx context.Context, args []string) error {
|
||||
policy, err := localClient.ReloadEffectivePolicy(ctx, setting.DefaultScope())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printPolicySettings(policy)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printPolicySettings(policy *setting.Snapshot) {
|
||||
if syspolicyArgs.json {
|
||||
json, err := json.MarshalIndent(policy, "", "\t")
|
||||
if err != nil {
|
||||
errf("syspolicy marshalling error: %v", err)
|
||||
} else {
|
||||
outln(string(json))
|
||||
}
|
||||
return
|
||||
}
|
||||
if policy.Len() == 0 {
|
||||
outln("No policy settings")
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "Name\tOrigin\tValue\tError")
|
||||
fmt.Fprintln(w, "----\t------\t-----\t-----")
|
||||
for _, k := range slices.Sorted(policy.Keys()) {
|
||||
setting, _ := policy.GetSetting(k)
|
||||
var origin string
|
||||
if o := setting.Origin(); o != nil {
|
||||
origin = o.String()
|
||||
}
|
||||
if err := setting.Error(); err != nil {
|
||||
fmt.Fprintf(w, "%s\t%s\t\t{%v}\n", k, origin, err)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t\n", k, origin, setting.Value())
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
243
cmd/tailscale/depaware-minlinux.txt
Normal file
243
cmd/tailscale/depaware-minlinux.txt
Normal file
@@ -0,0 +1,243 @@
|
||||
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
L filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
L filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
D github.com/google/uuid from tailscale.com/util/quarantine
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
L github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
|
||||
github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli+
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3/internal from github.com/peterbourgon/ff/v3
|
||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
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
|
||||
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/net/tsaddr
|
||||
software.sslmate.com/src/go-pkcs12 from tailscale.com/cmd/tailscale/cli
|
||||
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/web from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
L tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/drive from tailscale.com/client/tailscale+
|
||||
tailscale.com/envknob from tailscale.com/client/web+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web
|
||||
tailscale.com/health from tailscale.com/control/controlhttp+
|
||||
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/internal/noiseconn from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/licenses from tailscale.com/client/web+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/capture
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/sockstats from tailscale.com/net/portmapper
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/ipn+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/lazy from tailscale.com/version
|
||||
tailscale.com/types/logger from tailscale.com/client/web+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/nettype from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/views from tailscale.com/client/web+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cmpver from tailscale.com/clientupdate
|
||||
tailscale.com/util/ctxkey from tailscale.com/types/logger
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineiter from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
|
||||
tailscale.com/util/must from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/truncate from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
tailscale.com/version from tailscale.com/client/web+
|
||||
tailscale.com/version/distro from tailscale.com/client/web+
|
||||
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from tailscale.com/clientupdate/distsign+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/internal/hpke+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/hkdf from crypto/internal/hpke+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
golang.org/x/net/http/httpproxy from net/http+
|
||||
golang.org/x/net/http2 from tailscale.com/cmd/tailscale/cli+
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
|
||||
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2+
|
||||
golang.org/x/sync/errgroup from github.com/tailscale/goupnp/httpu
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
golang.org/x/sys/unix from github.com/mattn/go-isatty+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli
|
||||
archive/tar from tailscale.com/clientupdate
|
||||
bufio from compress/flate+
|
||||
bytes from archive/tar+
|
||||
cmp from encoding/json+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from golang.org/x/net/http2+
|
||||
compress/zlib from image/png
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdh+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdh from crypto/ecdsa+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from golang.org/x/net/http2+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
D database/sql/driver from github.com/google/uuid
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/gob+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/gob from github.com/gorilla/securecookie
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from github.com/google/uuid+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from archive/tar+
|
||||
flag from github.com/peterbourgon/ff/v3+
|
||||
fmt from archive/tar+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from html/template
|
||||
html/template from github.com/gorilla/csrf
|
||||
image from github.com/skip2/go-qrcode+
|
||||
image/color from github.com/skip2/go-qrcode+
|
||||
image/png from github.com/skip2/go-qrcode
|
||||
io from archive/tar+
|
||||
io/fs from archive/tar+
|
||||
io/ioutil from github.com/mitchellh/go-ps+
|
||||
iter from maps+
|
||||
log from github.com/skip2/go-qrcode+
|
||||
log/internal from log
|
||||
maps from net/http+
|
||||
math from archive/tar+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from golang.org/x/net/http2+
|
||||
math/rand/v2 from internal/concurrent+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from github.com/gorilla/csrf+
|
||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||
net/http/httptrace from golang.org/x/net/http2+
|
||||
net/http/httputil from tailscale.com/client/web+
|
||||
net/http/internal from net/http+
|
||||
net/netip from go4.org/netipx+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from net/http/cgi+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from archive/tar+
|
||||
path from archive/tar+
|
||||
path/filepath from archive/tar+
|
||||
reflect from archive/tar+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from tailscale.com+
|
||||
slices from archive/tar+
|
||||
sort from compress/flate+
|
||||
strconv from archive/tar+
|
||||
strings from archive/tar+
|
||||
sync from archive/tar+
|
||||
sync/atomic from context+
|
||||
syscall from archive/tar+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v3/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from archive/tar+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
unique from net/netip
|
||||
@@ -1,230 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.19
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
ifconfig bool // print network state once and exit
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
portmap bool
|
||||
}
|
||||
|
||||
var debugModeFunc = debugMode // so it can be addressable
|
||||
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fs.Args()) > 0 {
|
||||
return errors.New("unknown non-flag debug subcommand arguments")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.ifconfig {
|
||||
return runMonitor(ctx, false)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx, true)
|
||||
}
|
||||
if debugArgs.portmap {
|
||||
return debugPortmap(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context, loop bool) error {
|
||||
dump := func(st *netmon.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := netmon.New(log.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mon.Close()
|
||||
|
||||
mon.RegisterChangeCallback(func(delta *netmon.ChangeDelta) {
|
||||
if !delta.Major {
|
||||
log.Printf("Network monitor fired; not a major change")
|
||||
return
|
||||
}
|
||||
log.Printf("Network monitor fired. New state:")
|
||||
dump(delta.New)
|
||||
})
|
||||
if loop {
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
}
|
||||
dump(mon.InterfaceState())
|
||||
if !loop {
|
||||
return nil
|
||||
}
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) got: auth of %d bytes, err=%v", proxyURL, len(auth), err)
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
if auth != "" {
|
||||
// We used log.Printf above (for timestamps).
|
||||
// Use fmt.Printf here instead just to appease
|
||||
// a security scanner, despite log.Printf only
|
||||
// going to stdout.
|
||||
fmt.Printf("... Proxy-Authorization = %q\n", auth)
|
||||
}
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) (err error) {
|
||||
ht := new(health.Tracker)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create derp map request: %w", err)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("fetch derp map: %v: %s", res.Status, b)
|
||||
}
|
||||
var dmap tailcfg.DERPMap
|
||||
if err = json.Unmarshal(b, &dmap); err != nil {
|
||||
return fmt.Errorf("fetch DERP map: %w", err)
|
||||
}
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewNode()
|
||||
priv2 := key.NewNode()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
|
||||
c1.HealthTracker = ht
|
||||
c2.HealthTracker = ht
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
|
||||
func debugPortmap(ctx context.Context) error {
|
||||
return fmt.Errorf("this flag has been deprecated in favour of 'tailscale debug portmap'")
|
||||
}
|
||||
237
cmd/tailscaled/depaware-minlinux.txt
Normal file
237
cmd/tailscaled/depaware-minlinux.txt
Normal file
@@ -0,0 +1,237 @@
|
||||
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
|
||||
github.com/gaissmai/bart from tailscale.com/net/ipset+
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/huff0+
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/net/tstun+
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/localapi
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
|
||||
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/disco from tailscale.com/net/tstun+
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/store
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netutil from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/packet from tailscale.com/net/packet/checksum+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/omit from tailscale.com/ipn/conffile
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tstime from tailscale.com/control/controlclient+
|
||||
tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/empty from tailscale.com/ipn+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/ipn+
|
||||
tailscale.com/types/key from tailscale.com/control/controlbase+
|
||||
tailscale.com/types/lazy from tailscale.com/version
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/opt from tailscale.com/control/controlknobs+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/ptr from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/views from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/ctxkey from tailscale.com/types/logger
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/execqueue from tailscale.com/control/controlclient
|
||||
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/ipn/localapi
|
||||
tailscale.com/util/lineiter 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/cmd/tailscaled+
|
||||
tailscale.com/util/must from tailscale.com/util/zstdframe
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/set from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient
|
||||
tailscale.com/util/slicesx from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/testenv from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
tailscale.com/util/zstdframe from tailscale.com/control/controlclient
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/internal/hpke+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/hkdf from crypto/internal/hpke+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
|
||||
golang.org/x/net/bpf from golang.org/x/net/ipv4+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/conn+
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/conn+
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sys/cpu from github.com/tailscale/wireguard-go/tun+
|
||||
golang.org/x/sys/unix from github.com/tailscale/wireguard-go/conn+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/derp+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
cmp from encoding/json+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from golang.org/x/net/http2+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdh+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdh from crypto/ecdsa+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from golang.org/x/net/http2+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from github.com/bits-and-blooms/bitset+
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
flag from net/http/httptest+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
io from bufio+
|
||||
io/fs from crypto/x509+
|
||||
iter from maps+
|
||||
log from github.com/klauspost/compress/zstd+
|
||||
log/internal from log
|
||||
maps from net/http+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from golang.org/x/net/http2+
|
||||
math/rand/v2 from internal/concurrent+
|
||||
mime from mime/multipart+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from golang.org/x/net/http2+
|
||||
net/http/httptest from tailscale.com/control/controlclient
|
||||
net/http/httptrace from golang.org/x/net/http2+
|
||||
net/http/httputil from tailscale.com/cmd/tailscaled
|
||||
net/http/internal from net/http+
|
||||
net/netip from github.com/gaissmai/bart+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from tailscale.com/cmd/tailscaled+
|
||||
os/signal from tailscale.com/cmd/tailscaled
|
||||
os/user from tailscale.com/ipn/ipnserver
|
||||
path from io/fs+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
slices from crypto/tls+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
unique from net/netip
|
||||
70
cmd/tailscaled/netstack.go
Normal file
70
cmd/tailscaled/netstack.go
Normal file
@@ -0,0 +1,70 @@
|
||||
//go:build !ts_omit_netstack
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"expvar"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System, onlyNetstack, handleSubnetsInNetstack bool) (start func(localBackend any) error, err error) {
|
||||
ns, err := netstack.Create(logf,
|
||||
sys.Tun.Get(),
|
||||
sys.Engine.Get(),
|
||||
sys.MagicSock.Get(),
|
||||
sys.Dialer.Get(),
|
||||
sys.DNSManager.Get(),
|
||||
sys.ProxyMapper(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Only register debug info if we have a debug mux
|
||||
if debugMux != nil {
|
||||
expvar.Publish("netstack", ns.ExpVar())
|
||||
}
|
||||
sys.Set(ns)
|
||||
ns.ProcessLocalIPs = onlyNetstack
|
||||
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack
|
||||
|
||||
dialer := sys.Dialer.Get()
|
||||
|
||||
if onlyNetstack {
|
||||
e := sys.Engine.Get()
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
_, ok := e.PeerForIP(ip)
|
||||
return ok
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextTCP or we'll return
|
||||
// *gonet.TCPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
tcpConn, err := ns.DialContextTCP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
}
|
||||
dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextUDP or we'll return
|
||||
// *gonet.UDPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
udpConn, err := ns.DialContextUDP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
}
|
||||
}
|
||||
|
||||
return func(lbAny any) error {
|
||||
return ns.Start(lbAny.(*ipnlocal.LocalBackend))
|
||||
}, nil
|
||||
}
|
||||
12
cmd/tailscaled/netstack_disabled.go
Normal file
12
cmd/tailscaled/netstack_disabled.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build ts_omit_netstack
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System, onlyNetstack, handleSubnetsInNetstack bool) (start func(localBackend any) error, err error) {
|
||||
return func(any) error { return nil }, nil
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.19
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
|
||||
dg := distro.Get()
|
||||
switch dg {
|
||||
case distro.Synology, distro.TrueNAS, distro.QNAP, distro.Unraid:
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findTaildropDir(dg)
|
||||
if err != nil {
|
||||
logf("%s Taildrop support: %v", dg, err)
|
||||
} else {
|
||||
logf("%s Taildrop: using %v", dg, path)
|
||||
lb.SetDirectFileRoot(path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findTaildropDir(dg distro.Distro) (string, error) {
|
||||
const name = "Taildrop"
|
||||
switch dg {
|
||||
case distro.Synology:
|
||||
return findSynologyTaildropDir(name)
|
||||
case distro.TrueNAS:
|
||||
return findTrueNASTaildropDir(name)
|
||||
case distro.QNAP:
|
||||
return findQnapTaildropDir(name)
|
||||
case distro.Unraid:
|
||||
return findUnraidTaildropDir(name)
|
||||
}
|
||||
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
|
||||
}
|
||||
|
||||
// findSynologyTaildropDir looks for the first volume containing a
|
||||
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
|
||||
// but on DSM7 at least, we lack permissions to run that.
|
||||
func findSynologyTaildropDir(name string) (dir string, err error) {
|
||||
for i := 1; i <= 16; i++ {
|
||||
dir = fmt.Sprintf("/volume%v/%s", i, name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findTrueNASTaildropDir returns the first matching directory of
|
||||
// /mnt/{name} or /mnt/*/{name}
|
||||
func findTrueNASTaildropDir(name string) (dir string, err error) {
|
||||
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
|
||||
dir = fmt.Sprintf("/mnt/%s", name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// but if running on the host, it may be something like /mnt/Primary/Taildrop
|
||||
fis, err := os.ReadDir("/mnt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading /mnt: %w", err)
|
||||
}
|
||||
for _, fi := range fis {
|
||||
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findQnapTaildropDir checks if a Shared Folder named "Taildrop" exists.
|
||||
func findQnapTaildropDir(name string) (string, error) {
|
||||
dir := fmt.Sprintf("/share/%s", name)
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// share/Taildrop is usually a symlink to CACHEDEV1_DATA/Taildrop/ or some such.
|
||||
fullpath, err := filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("symlink to shared folder %q not found", name)
|
||||
}
|
||||
if fi, err = os.Stat(fullpath); err == nil && fi.IsDir() {
|
||||
return dir, nil // return the symlink, how QNAP set it up
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findUnraidTaildropDir looks for a directory linked at
|
||||
// /var/lib/tailscale/Taildrop. This is a symlink to the
|
||||
// path specified by the user in the Unraid Web UI
|
||||
func findUnraidTaildropDir(name string) (string, error) {
|
||||
dir := fmt.Sprintf("/var/lib/tailscale/%s", name)
|
||||
_, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("symlink %q not found", name)
|
||||
}
|
||||
|
||||
fullpath, err := filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("symlink %q to shared folder not valid", name)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(fullpath)
|
||||
if err == nil && fi.IsDir() {
|
||||
return dir, nil // return the symlink
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
@@ -13,14 +13,10 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -30,10 +26,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
@@ -41,32 +34,20 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
@@ -120,7 +101,6 @@ var args struct {
|
||||
|
||||
cleanUp bool
|
||||
confFile string // empty, file path, or "vm:user-data"
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
statedir string
|
||||
@@ -145,23 +125,18 @@ var (
|
||||
var subCommands = map[string]*func([]string) error{
|
||||
"install-system-daemon": &installSystemDaemon,
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
"be-child": &beChildFunc,
|
||||
"serve-taildrive": &serveDriveFunc,
|
||||
}
|
||||
|
||||
var beCLI func() // non-nil if CLI is linked in
|
||||
|
||||
func main() {
|
||||
envknob.PanicIfAnyEnvCheckedInInit()
|
||||
envknob.ApplyDiskConfig()
|
||||
applyIntegrationTestEnvKnob()
|
||||
|
||||
defaultVerbosity := envknob.RegisterInt("TS_LOG_VERBOSITY")
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", defaultVerbosity(), "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanUp, "cleanup", false, "clean up system state and exit")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
@@ -242,9 +217,6 @@ func main() {
|
||||
|
||||
err := run()
|
||||
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -332,9 +304,6 @@ func ipnServerOpts() (o serverOptions) {
|
||||
return o
|
||||
}
|
||||
|
||||
var logPol *logpolicy.Policy
|
||||
var debugMux *http.ServeMux
|
||||
|
||||
func run() (err error) {
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
@@ -362,24 +331,10 @@ func run() (err error) {
|
||||
sys.Set(netMon)
|
||||
}
|
||||
|
||||
pol := logpolicy.New(logtail.CollectionNode, netMon, sys.HealthTracker(), nil /* use log.Printf */)
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
logPol = pol
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
|
||||
if isWinSvc {
|
||||
// Run the IPN server from the Windows service manager.
|
||||
log.Printf("Running service...")
|
||||
if err := runWindowsService(pol); err != nil {
|
||||
if err := runWindowsService(); err != nil {
|
||||
log.Printf("runservice: %v", err)
|
||||
}
|
||||
log.Printf("Service ended.")
|
||||
@@ -394,10 +349,6 @@ func run() (err error) {
|
||||
if envknob.Bool("TS_PLEASE_PANIC") {
|
||||
panic("TS_PLEASE_PANIC asked us to panic")
|
||||
}
|
||||
// Always clean up, even if we're going to run the server. This covers cases
|
||||
// such as when a system was rebooted without shutting down, or tailscaled
|
||||
// crashed, and would for example restore system DNS configuration.
|
||||
dns.CleanUp(logf, netMon, sys.HealthTracker(), args.tunname)
|
||||
router.CleanUp(logf, netMon, args.tunname)
|
||||
// If the cleanUp flag was passed, then exit.
|
||||
if args.cleanUp {
|
||||
@@ -411,22 +362,16 @@ func run() (err error) {
|
||||
log.Printf("error in synology migration: %v", err)
|
||||
}
|
||||
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
sys.Set(driveimpl.NewFileSystemForRemote(logf))
|
||||
|
||||
if app := envknob.App(); app != "" {
|
||||
hostinfo.SetApp(app)
|
||||
}
|
||||
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
||||
return startIPNServer(context.Background(), logf, sys)
|
||||
}
|
||||
|
||||
var sigPipe os.Signal // set by sigpipe.go
|
||||
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, sys *tsd.System) error {
|
||||
ln, err := safesocket.Listen(args.socketpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
@@ -467,10 +412,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
|
||||
}
|
||||
}()
|
||||
|
||||
srv := ipnserver.New(logf, logID, sys.NetMon.Get())
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
srv := ipnserver.New(logf, sys.NetMon.Get())
|
||||
var lbErr syncs.AtomicValue[error]
|
||||
|
||||
go func() {
|
||||
@@ -485,7 +427,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
|
||||
return
|
||||
}
|
||||
}
|
||||
lb, err := getLocalBackend(ctx, logf, logID, sys)
|
||||
lb, err := getLocalBackend(ctx, logf, sys)
|
||||
if err == nil {
|
||||
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
||||
if lb.Prefs().Valid() {
|
||||
@@ -519,12 +461,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
if logPol != nil {
|
||||
logPol.Logtail.SetNetMon(sys.NetMon.Get())
|
||||
}
|
||||
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
|
||||
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
sys.Set(dialer)
|
||||
@@ -533,69 +470,11 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if debugMux != nil {
|
||||
if ms, ok := sys.MagicSock.GetOK(); ok {
|
||||
debugMux.HandleFunc("/debug/magicsock", ms.ServeHTTPDebug)
|
||||
}
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, sys)
|
||||
startNetstack, err := newNetstack(logf, sys, onlyNetstack, handleSubnetsInNetstack())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
sys.Set(ns)
|
||||
ns.ProcessLocalIPs = onlyNetstack
|
||||
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack()
|
||||
|
||||
if onlyNetstack {
|
||||
e := sys.Engine.Get()
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
_, ok := e.PeerForIP(ip)
|
||||
return ok
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextTCP or we'll return
|
||||
// *gonet.TCPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
tcpConn, err := ns.DialContextTCP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
}
|
||||
dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextUDP or we'll return
|
||||
// *gonet.UDPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
udpConn, err := ns.DialContextUDP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
}
|
||||
}
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
var addrs []string
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||
go func() {
|
||||
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
||||
}()
|
||||
addrs = append(addrs, httpProxyListener.Addr().String())
|
||||
}
|
||||
if socksListener != nil {
|
||||
ss := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
Dialer: dialer.UserDial,
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||
}()
|
||||
addrs = append(addrs, socksListener.Addr().String())
|
||||
}
|
||||
tshttpproxy.SetSelfProxy(addrs...)
|
||||
}
|
||||
|
||||
opts := ipnServerOpts()
|
||||
|
||||
@@ -609,23 +488,15 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
w.Start()
|
||||
}
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, sys, opts.LoginFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
}
|
||||
lb.SetVarRoot(opts.VarRoot)
|
||||
if logPol != nil {
|
||||
lb.SetLogFlusher(logPol.Logtail.StartFlush)
|
||||
}
|
||||
if root := lb.TailscaleVarRoot(); root != "" {
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
|
||||
// lanscaping dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
|
||||
}
|
||||
lb.ConfigureWebClient(&tailscale.LocalClient{
|
||||
Socket: args.socketpath,
|
||||
UseSocketOnly: args.socketpath != paths.DefaultTailscaledSocket(),
|
||||
})
|
||||
configureTaildrop(logf, lb)
|
||||
if err := ns.Start(lb); err != nil {
|
||||
if err := startNetstack(lb); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
return lb, nil
|
||||
@@ -680,15 +551,11 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
HealthTracker: sys.HealthTracker(),
|
||||
Metrics: sys.UserMetricsRegistry(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
DriveForLocal: driveimpl.NewFileSystemForLocal(logf),
|
||||
}
|
||||
|
||||
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
|
||||
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
netstackSubnetRouter := onlyNetstack // but mutated later on some platforms
|
||||
netns.SetEnabled(!onlyNetstack)
|
||||
@@ -702,19 +569,9 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
||||
}
|
||||
if onlyNetstack {
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
// On Synology in netstack mode, still init a DNS
|
||||
// manager (directManager) to avoid the health check
|
||||
// warnings in 'tailscale status' about DNS base
|
||||
// configuration being unavailable (from the noop
|
||||
// manager). More in Issue 4017.
|
||||
// TODO(bradfitz): add a Synology-specific DNS manager.
|
||||
conf.DNS, err = dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), "") // empty interface name
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dev, devName, err := tstunNew(logf, name)
|
||||
dev, _, err := tstunNew(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name, err)
|
||||
return false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
@@ -736,13 +593,6 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
||||
return false, fmt.Errorf("creating router: %w", err)
|
||||
}
|
||||
|
||||
d, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), devName)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
r.Close()
|
||||
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if handleSubnetsInNetstack() {
|
||||
netstackSubnetRouter = true
|
||||
@@ -760,23 +610,6 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
||||
return onlyNetstack, nil
|
||||
}
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/metrics", servePrometheusMetrics)
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
return mux
|
||||
}
|
||||
|
||||
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
varz.Handler(w, r)
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
@@ -787,112 +620,6 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
|
||||
ret, err := netstack.Create(logf,
|
||||
sys.Tun.Get(),
|
||||
sys.Engine.Get(),
|
||||
sys.MagicSock.Get(),
|
||||
sys.Dialer.Get(),
|
||||
sys.DNSManager.Get(),
|
||||
sys.ProxyMapper(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Only register debug info if we have a debug mux
|
||||
if debugMux != nil {
|
||||
expvar.Publish("netstack", ret.ExpVar())
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
|
||||
// proxies, if the respective addresses are not empty. socksAddr and
|
||||
// httpAddr can be the same, in which case socksListener will receive
|
||||
// connections that look like they're speaking SOCKS and httpListener
|
||||
// will receive everything else.
|
||||
//
|
||||
// socksListener and httpListener can be nil, if their respective
|
||||
// addrs are empty.
|
||||
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
|
||||
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
|
||||
ln, err := net.Listen("tcp", socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("proxy listener: %v", err)
|
||||
}
|
||||
return proxymux.SplitSOCKSAndHTTP(ln)
|
||||
}
|
||||
|
||||
var err error
|
||||
if socksAddr != "" {
|
||||
socksListener, err = net.Listen("tcp", socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(socksAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
||||
}
|
||||
}
|
||||
if httpAddr != "" {
|
||||
httpListener, err = net.Listen("tcp", httpAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("HTTP proxy listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(httpAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
return socksListener, httpListener
|
||||
}
|
||||
|
||||
var beChildFunc = beChild
|
||||
|
||||
func beChild(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("missing mode argument")
|
||||
}
|
||||
typ := args[0]
|
||||
f, ok := childproc.Code[typ]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown be-child mode %q", typ)
|
||||
}
|
||||
return f(args[1:])
|
||||
}
|
||||
|
||||
var serveDriveFunc = serveDrive
|
||||
|
||||
// serveDrive serves one or more Taildrives on localhost using the WebDAV
|
||||
// protocol. On UNIX and MacOS tailscaled environment, Taildrive spawns child
|
||||
// tailscaled processes in serve-taildrive mode in order to access the fliesystem
|
||||
// as specific (usually unprivileged) users.
|
||||
//
|
||||
// serveDrive prints the address on which it's listening to stdout so that the
|
||||
// parent process knows where to connect to.
|
||||
func serveDrive(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("missing shares")
|
||||
}
|
||||
if len(args)%2 != 0 {
|
||||
return errors.New("need <sharename> <path> pairs")
|
||||
}
|
||||
s, err := driveimpl.NewFileServer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start Taildrive file server: %v", err)
|
||||
}
|
||||
shares := make(map[string]string)
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
shares[args[i]] = args[i+1]
|
||||
}
|
||||
s.SetShares(shares)
|
||||
fmt.Printf("%v\n", s.Addr())
|
||||
return s.Serve()
|
||||
}
|
||||
|
||||
// dieOnPipeReadErrorOfFD reads from the pipe named by fd and exit the process
|
||||
// when the pipe becomes readable. We use this in tests as a somewhat more
|
||||
// portable mechanism for the Linux PR_SET_PDEATHSIG, which we wish existed on
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import "tailscale.com/logpolicy"
|
||||
|
||||
func isWindowsService() bool { return false }
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
|
||||
func runWindowsService() error { panic("unreachable") }
|
||||
|
||||
func beWindowsSubprocess() bool { return false }
|
||||
|
||||
@@ -44,9 +44,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tsd"
|
||||
@@ -129,7 +127,7 @@ var syslogf logger.Logf = logger.Discard
|
||||
//
|
||||
// At this point we're still the parent process that
|
||||
// Windows started.
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
func runWindowsService() error {
|
||||
go func() {
|
||||
logger.Logf(log.Printf).JSON(1, "SupportInfo", osdiag.SupportInfo(osdiag.LogSupportInfoReasonStartup))
|
||||
}()
|
||||
@@ -149,7 +147,6 @@ func runWindowsService(pol *logpolicy.Policy) error {
|
||||
}
|
||||
|
||||
type ipnService struct {
|
||||
Policy *logpolicy.Policy
|
||||
}
|
||||
|
||||
// Called by Windows to execute the windows service.
|
||||
@@ -369,12 +366,6 @@ func handleSessionChange(chgRequest svc.ChangeRequest) {
|
||||
|
||||
if flushDNSOnSessionUnlock, _ := syspolicy.GetBoolean(syspolicy.FlushDNSOnSessionUnlock, false); flushDNSOnSessionUnlock {
|
||||
log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
|
||||
go func() {
|
||||
err := dns.Flush()
|
||||
if err != nil {
|
||||
log.Printf("Error flushing DNS on session unlock: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -188,10 +187,8 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
observer: opts.Observer,
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.unregisterHealthWatch = opts.HealthTracker.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
@@ -258,7 +255,6 @@ func (c *Auto) cancelAuthCtxLocked() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +265,6 @@ func (c *Auto) cancelMapCtxLocked() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,12 +720,6 @@ func (c *Auto) TestOnlyTimeNow() time.Time {
|
||||
return c.clock.Now()
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
return c.direct.SetDNS(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
return c.direct.DoNoiseRequest(req)
|
||||
}
|
||||
|
||||
@@ -33,28 +33,20 @@ import (
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/zstdframe"
|
||||
)
|
||||
@@ -64,7 +56,6 @@ type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
@@ -246,13 +237,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
opts.Logf = log.Printf
|
||||
}
|
||||
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.MakeLookupFunc(opts.Logf, netMon),
|
||||
Logf: opts.Logf,
|
||||
}
|
||||
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil && runtime.GOOS == "js" {
|
||||
// In js/wasm, net/http.Transport (as of Go 1.18) will
|
||||
@@ -264,13 +248,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
var interceptedDial *atomic.Bool
|
||||
if httpc == nil {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), opts.HealthTracker, tr.TLSClientConfig)
|
||||
var dialFunc dialFunc
|
||||
dialFunc, interceptedDial = makeScreenTimeDetectingDialFunc(opts.Dialer.SystemDial)
|
||||
tr.DialContext = dnscache.Dialer(dialFunc, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialFunc, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
// Disable implicit gzip compression; the various
|
||||
// handlers (register, map, set-dns, etc) do their own
|
||||
@@ -302,7 +280,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
onControlTime: opts.OnControlTime,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
c.closedCtx, c.closeCtx = context.WithCancel(context.Background())
|
||||
@@ -433,13 +410,12 @@ func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newURL string, e
|
||||
}
|
||||
|
||||
func (c *Direct) doLoginOrRegen(ctx context.Context, opt loginOpt) (newURL string, err error) {
|
||||
mustRegen, url, oldNodeKeySignature, err := c.doLogin(ctx, opt)
|
||||
mustRegen, url, _, err := c.doLogin(ctx, opt)
|
||||
if err != nil {
|
||||
return url, err
|
||||
}
|
||||
if mustRegen {
|
||||
opt.Regen = true
|
||||
opt.OldNodeKeySignature = oldNodeKeySignature
|
||||
_, url, _, err = c.doLogin(ctx, opt)
|
||||
}
|
||||
return url, err
|
||||
@@ -465,10 +441,6 @@ type loginOpt struct {
|
||||
// It is ignored if Logout is set since Logout works by setting a
|
||||
// expiry time in the far past.
|
||||
Expiry *time.Time
|
||||
|
||||
// OldNodeKeySignature indicates the former NodeKeySignature
|
||||
// that must be resigned for the new node-key.
|
||||
OldNodeKeySignature tkatype.MarshaledSignature
|
||||
}
|
||||
|
||||
// hostInfoLocked returns a Clone of c.hostinfo and c.netinfo.
|
||||
@@ -489,7 +461,7 @@ var macOSScreenTime = health.Register(&health.Warnable{
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, nks tkatype.MarshaledSignature, err error) {
|
||||
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, Old any, err error) {
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
@@ -498,7 +470,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverLegacyKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := tka.DecodeWrappedAuthkey(c.authKey, c.logf)
|
||||
authKey := c.authKey
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
expired := !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
|
||||
@@ -518,7 +490,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
} else {
|
||||
if expired {
|
||||
c.logf("Old key expired -> regen=true")
|
||||
systemd.Status("key expired; run 'tailscale up' to authenticate")
|
||||
regen = true
|
||||
}
|
||||
if (opt.Flags & LoginInteractive) != 0 {
|
||||
@@ -574,10 +545,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if !persist.OldPrivateNodeKey.IsZero() {
|
||||
oldNodeKey = persist.OldPrivateNodeKey.Public()
|
||||
}
|
||||
if persist.NetworkLockKey.IsZero() {
|
||||
persist.NetworkLockKey = key.NewNLPrivate()
|
||||
}
|
||||
nlPub := persist.NetworkLockKey.Public()
|
||||
|
||||
if tryingNewKey.IsZero() {
|
||||
if opt.Logout {
|
||||
@@ -586,42 +553,20 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
log.Fatalf("tryingNewKey is empty, give up")
|
||||
}
|
||||
|
||||
var nodeKeySignature tkatype.MarshaledSignature
|
||||
if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != nil {
|
||||
if nodeKeySignature, err = tka.ResignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
|
||||
c.logf("Failed re-signing node-key signature: %v", err)
|
||||
}
|
||||
} else if isWrapped {
|
||||
// We were given a wrapped pre-auth key, which means that in addition
|
||||
// to being a regular pre-auth key there was a suffix with information to
|
||||
// generate a tailnet-lock signature.
|
||||
nodeKeySignature, err = tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public())
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if backendLogID == "" {
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
|
||||
tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
|
||||
if err != nil {
|
||||
c.logf("unable to provide Tailnet field in register request. err: %v", err)
|
||||
}
|
||||
now := c.clock.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
OldNodeKey: oldNodeKey,
|
||||
NodeKey: tryingNewKey.Public(),
|
||||
NLKey: nlPub,
|
||||
Hostinfo: hi,
|
||||
Followup: opt.URL,
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
NodeKeySignature: nodeKeySignature,
|
||||
Tailnet: tailnet,
|
||||
Version: 1,
|
||||
OldNodeKey: oldNodeKey,
|
||||
NodeKey: tryingNewKey.Public(),
|
||||
Hostinfo: hi,
|
||||
Followup: opt.URL,
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
}
|
||||
if opt.Logout {
|
||||
request.Expiry = time.Unix(123, 0) // far in the past
|
||||
@@ -630,7 +575,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
}
|
||||
c.logf("RegisterReq: onode=%v node=%v fup=%v nks=%v",
|
||||
request.OldNodeKey.ShortString(),
|
||||
request.NodeKey.ShortString(), opt.URL != "", len(nodeKeySignature) > 0)
|
||||
request.NodeKey.ShortString(), opt.URL != "", false)
|
||||
if authKey != "" {
|
||||
request.Auth = &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: authKey,
|
||||
@@ -703,9 +648,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if resp.Error != "" {
|
||||
return false, "", nil, UserVisibleError(resp.Error)
|
||||
}
|
||||
if len(resp.NodeKeySignature) > 0 {
|
||||
return true, "", resp.NodeKeySignature, nil
|
||||
}
|
||||
|
||||
if resp.NodeKeyExpired {
|
||||
if regen {
|
||||
@@ -1124,7 +1066,6 @@ func (c *Direct) handleDebugMessage(ctx context.Context, debug *tailcfg.Debug) e
|
||||
os.Exit(*code)
|
||||
}
|
||||
if debug.DisableLogTail {
|
||||
logtail.Disable()
|
||||
envknob.SetNoLogsNoSupport()
|
||||
}
|
||||
if sleep := time.Duration(debug.SleepSeconds * float64(time.Second)); sleep > 0 {
|
||||
@@ -1462,7 +1403,6 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
|
||||
ServerPubKey: serverNoiseKey,
|
||||
ServerURL: c.serverURL,
|
||||
Dialer: c.dialer,
|
||||
DNSCache: c.dnsCache,
|
||||
Logf: c.logf,
|
||||
NetMon: c.netMon,
|
||||
HealthTracker: c.health,
|
||||
@@ -1509,18 +1449,6 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
|
||||
metricSetDNS.Add(1)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
metricSetDNSError.Add(1)
|
||||
}
|
||||
}()
|
||||
return c.setDNSNoise(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
|
||||
@@ -82,13 +82,9 @@ type mapSession struct {
|
||||
lastPacketFilterRules views.Slice[tailcfg.FilterRule] // concatenation of all namedPacketFilters
|
||||
namedPacketFilters map[string]views.Slice[tailcfg.FilterRule]
|
||||
lastParsedPacketFilter []filter.Match
|
||||
lastSSHPolicy *tailcfg.SSHPolicy
|
||||
collectServices bool
|
||||
lastDomain string
|
||||
lastDomainAuditLogID string
|
||||
lastHealth []string
|
||||
lastPopBrowserURL string
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
lastNetmapSummary string // from NetworkMap.VeryConcise
|
||||
lastMaxExpiry time.Duration
|
||||
}
|
||||
@@ -325,9 +321,6 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
if c := resp.DNSConfig; c != nil {
|
||||
ms.lastDNSConfig = c
|
||||
}
|
||||
if p := resp.SSHPolicy; p != nil {
|
||||
ms.lastSSHPolicy = p
|
||||
}
|
||||
|
||||
if v, ok := resp.CollectServices.Get(); ok {
|
||||
ms.collectServices = v
|
||||
@@ -335,15 +328,6 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
if resp.Domain != "" {
|
||||
ms.lastDomain = resp.Domain
|
||||
}
|
||||
if resp.DomainDataPlaneAuditLogID != "" {
|
||||
ms.lastDomainAuditLogID = resp.DomainDataPlaneAuditLogID
|
||||
}
|
||||
if resp.Health != nil {
|
||||
ms.lastHealth = resp.Health
|
||||
}
|
||||
if resp.TKAInfo != nil {
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
if resp.MaxKeyDuration > 0 {
|
||||
ms.lastMaxExpiry = resp.MaxKeyDuration
|
||||
}
|
||||
@@ -474,10 +458,6 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
mut.KeyExpiry = *v
|
||||
patchKeyExpiry.Add(1)
|
||||
}
|
||||
if v := pc.KeySignature; v != nil {
|
||||
mut.KeySignature = v
|
||||
patchKeySignature.Add(1)
|
||||
}
|
||||
if v := pc.CapMap; v != nil {
|
||||
mut.CapMap = v
|
||||
patchCapMap.Add(1)
|
||||
@@ -607,10 +587,6 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
if !was.KeyExpiry().Equal(n.KeyExpiry) {
|
||||
pc().KeyExpiry = ptr.To(n.KeyExpiry)
|
||||
}
|
||||
case "KeySignature":
|
||||
if !was.KeySignature().Equal(n.KeySignature) {
|
||||
pc().KeySignature = slices.Clone(n.KeySignature)
|
||||
}
|
||||
case "Machine":
|
||||
if was.Machine() != n.Machine {
|
||||
return nil, false
|
||||
@@ -734,18 +710,6 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
if va == nil || vb == nil || *va != *vb {
|
||||
return nil, false
|
||||
}
|
||||
case "ExitNodeDNSResolvers":
|
||||
va, vb := was.ExitNodeDNSResolvers(), views.SliceOfViews(n.ExitNodeDNSResolvers)
|
||||
|
||||
if va.Len() != vb.Len() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for i := range va.Len() {
|
||||
if !va.At(i).Equal(vb.At(i)) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -776,25 +740,13 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
Peers: peerViews,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
PacketFilterRules: ms.lastPacketFilterRules,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
MaxKeyDuration: ms.lastMaxExpiry,
|
||||
}
|
||||
|
||||
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
|
||||
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
|
||||
ms.logf("error unmarshalling TKAHead: %v", err)
|
||||
nm.TKAEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
if node := ms.lastNode; node.Valid() {
|
||||
nm.SelfNode = node
|
||||
nm.Expiry = node.KeyExpiry()
|
||||
@@ -807,8 +759,5 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
ms.addUserProfile(nm, peer.Sharer())
|
||||
ms.addUserProfile(nm, peer.User())
|
||||
}
|
||||
if DevKnob.ForceProxyDNS() {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
return nm
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/noiseconn"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -54,7 +53,6 @@ type NoiseClient struct {
|
||||
sfDial singleflight.Group[struct{}, *noiseconn.Conn]
|
||||
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
privKey key.MachinePrivate
|
||||
serverPubKey key.MachinePublic
|
||||
host string // the host part of serverURL
|
||||
@@ -89,10 +87,6 @@ type NoiseOpts struct {
|
||||
ServerURL string
|
||||
// Dialer's SystemDial function is used to connect to the server.
|
||||
Dialer *tsdial.Dialer
|
||||
// DNSCache is the caching Resolver to use to connect to the server.
|
||||
//
|
||||
// This field can be nil.
|
||||
DNSCache *dnscache.Resolver
|
||||
// Logf is the log function to use. This field can be nil.
|
||||
Logf logger.Logf
|
||||
// NetMon is the network monitor that, if set, will be used to get the
|
||||
@@ -158,7 +152,6 @@ func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: opts.DNSCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
logf: opts.Logf,
|
||||
netMon: opts.NetMon,
|
||||
@@ -364,8 +357,6 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
MachineKey: nc.privKey,
|
||||
ControlKey: nc.serverPubKey,
|
||||
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
|
||||
Dialer: nc.dialer.SystemDial,
|
||||
DNSCache: nc.dnsCache,
|
||||
DialPlan: dialPlan,
|
||||
Logf: nc.logf,
|
||||
NetMon: nc.netMon,
|
||||
|
||||
@@ -41,12 +41,8 @@ import (
|
||||
"tailscale.com/control/controlhttp/controlhttpcommon"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
@@ -77,13 +73,6 @@ func (a *Dialer) logf(format string, args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
if a.proxyFunc != nil {
|
||||
return a.proxyFunc
|
||||
}
|
||||
return tshttpproxy.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
|
||||
// starting to try a.HTTPSPort.
|
||||
func (a *Dialer) httpsFallbackDelay() time.Duration {
|
||||
@@ -306,8 +295,6 @@ func (a *Dialer) dialHost(ctx context.Context, optAddr netip.Addr) (*ClientConn,
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer, a.logf)
|
||||
|
||||
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
|
||||
// respectively, in order to do the HTTP upgrade to a net.Conn over which
|
||||
// we'll speak Noise.
|
||||
@@ -422,21 +409,6 @@ func (a *Dialer) dialURL(ctx context.Context, u *url.URL, optAddr netip.Addr) (*
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolver returns a.DNSCache if non-nil or a new *dnscache.Resolver
|
||||
// otherwise.
|
||||
func (a *Dialer) resolver() *dnscache.Resolver {
|
||||
if a.DNSCache != nil {
|
||||
return a.DNSCache
|
||||
}
|
||||
|
||||
return &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
|
||||
UseLastGood: true,
|
||||
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
|
||||
}
|
||||
}
|
||||
|
||||
func isLoopback(a net.Addr) bool {
|
||||
if ta, ok := a.(*net.TCPAddr); ok {
|
||||
return ta.IP.IsLoopback()
|
||||
@@ -461,26 +433,9 @@ var macOSScreenTime = health.Register(&health.Warnable{
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Addr, init []byte) (_ net.Conn, retErr error) {
|
||||
var dns *dnscache.Resolver
|
||||
|
||||
// If we were provided an address to dial, then create a resolver that just
|
||||
// returns that value; otherwise, fall back to DNS.
|
||||
if optAddr.IsValid() {
|
||||
dns = &dnscache.Resolver{
|
||||
SingleHostStaticResult: []netip.Addr{optAddr},
|
||||
SingleHost: u.Hostname(),
|
||||
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
|
||||
}
|
||||
} else {
|
||||
dns = a.resolver()
|
||||
}
|
||||
|
||||
var dialer dnscache.DialContextFunc
|
||||
if a.Dialer != nil {
|
||||
dialer = a.Dialer
|
||||
} else {
|
||||
dialer = stdDialer.DialContext
|
||||
}
|
||||
var dialer func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
dialer = stdDialer.DialContext
|
||||
|
||||
// On macOS, see if Screen Time is blocking things.
|
||||
if runtime.GOOS == "darwin" {
|
||||
@@ -508,9 +463,6 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
|
||||
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
defer tr.CloseIdleConnections()
|
||||
tr.Proxy = a.getProxyFunc()
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer, dns)
|
||||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{}
|
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
||||
@@ -533,7 +485,6 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
|
||||
return nil // regardless
|
||||
}
|
||||
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
|
||||
tr.DisableCompression = true
|
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
@@ -62,16 +61,6 @@ type Dialer struct {
|
||||
// If "none" (NoPort), HTTPS is disabled.
|
||||
HTTPSPort string
|
||||
|
||||
// Dialer is the dialer used to make outbound connections.
|
||||
//
|
||||
// If not specified, this defaults to net.Dialer.DialContext.
|
||||
Dialer dnscache.DialContextFunc
|
||||
|
||||
// DNSCache is the caching Resolver used by this Dialer.
|
||||
//
|
||||
// If not specified, a new Resolver is created per attempt.
|
||||
DNSCache *dnscache.Resolver
|
||||
|
||||
// Logf, if set, is a logging function to use; if unset, logs are
|
||||
// dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -22,6 +23,17 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Conn is the subset of the underlying net.Conn the DERP Server needs.
|
||||
// It is a defined type so that non-net connections can be used.
|
||||
type Conn interface {
|
||||
io.WriteCloser
|
||||
LocalAddr() net.Addr
|
||||
// The *Deadline methods follow the semantics of net.Conn.
|
||||
SetDeadline(time.Time) error
|
||||
SetReadDeadline(time.Time) error
|
||||
SetWriteDeadline(time.Time) error
|
||||
}
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.NodePublic // of the DERP server; not a machine or node key
|
||||
@@ -140,6 +152,13 @@ func (c *Client) recvServerKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type serverInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
|
||||
TokenBucketBytesPerSecond int `json:",omitempty"`
|
||||
TokenBucketBytesBurst int `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
const maxLength = nonceLen + maxInfoLen
|
||||
fl := len(b)
|
||||
|
||||
2315
derp/derp_server.go
2315
derp/derp_server.go
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package derp
|
||||
|
||||
import "context"
|
||||
|
||||
func (c *sclient) startStatsLoop(ctx context.Context) {
|
||||
// Nothing to do
|
||||
return
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package derp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/tcpinfo"
|
||||
)
|
||||
|
||||
func (c *sclient) startStatsLoop(ctx context.Context) {
|
||||
// Get the RTT initially to verify it's supported.
|
||||
conn := c.tcpConn()
|
||||
if conn == nil {
|
||||
c.s.tcpRtt.Add("non-tcp", 1)
|
||||
return
|
||||
}
|
||||
if _, err := tcpinfo.RTT(conn); err != nil {
|
||||
c.logf("error fetching initial RTT: %v", err)
|
||||
c.s.tcpRtt.Add("error", 1)
|
||||
return
|
||||
}
|
||||
|
||||
const statsInterval = 10 * time.Second
|
||||
|
||||
// Don't launch a goroutine; use a timer instead.
|
||||
var gatherStats func()
|
||||
gatherStats = func() {
|
||||
// Do nothing if the context is finished.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Reschedule ourselves when this stats gathering is finished.
|
||||
defer c.s.clock.AfterFunc(statsInterval, gatherStats)
|
||||
|
||||
// Gather TCP RTT information.
|
||||
rtt, err := tcpinfo.RTT(conn)
|
||||
if err == nil {
|
||||
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
|
||||
}
|
||||
|
||||
// TODO(andrew): more metrics?
|
||||
}
|
||||
|
||||
// Kick off the initial timer.
|
||||
c.s.clock.AfterFunc(statsInterval, gatherStats)
|
||||
}
|
||||
|
||||
// tcpConn attempts to get the underlying *net.TCPConn from this client's
|
||||
// Conn; if it cannot, then it will return nil.
|
||||
func (c *sclient) tcpConn() *net.TCPConn {
|
||||
nc := c.nc
|
||||
for {
|
||||
switch v := nc.(type) {
|
||||
case *net.TCPConn:
|
||||
return v
|
||||
case *tls.Conn:
|
||||
nc = v.NetConn()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func durationToLabel(dur time.Duration) string {
|
||||
switch {
|
||||
case dur <= 10*time.Millisecond:
|
||||
return "10ms"
|
||||
case dur <= 20*time.Millisecond:
|
||||
return "20ms"
|
||||
case dur <= 50*time.Millisecond:
|
||||
return "50ms"
|
||||
case dur <= 100*time.Millisecond:
|
||||
return "100ms"
|
||||
case dur <= 150*time.Millisecond:
|
||||
return "150ms"
|
||||
case dur <= 250*time.Millisecond:
|
||||
return "250ms"
|
||||
case dur <= 500*time.Millisecond:
|
||||
return "500ms"
|
||||
default:
|
||||
return "inf"
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,9 @@ import (
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
@@ -52,11 +49,10 @@ import (
|
||||
// Send/Recv will completely re-establish the connection (unless Close
|
||||
// has been called).
|
||||
type Client struct {
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
HealthTracker *health.Tracker // optional; used if non-nil only
|
||||
DNSCache *dnscache.Resolver // optional; nil means no caching
|
||||
MeshKey string // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
HealthTracker *health.Tracker // optional; used if non-nil only
|
||||
MeshKey string // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
// WatchConnectionChanges is whether the client wishes to subscribe to
|
||||
// notifications about clients connecting & disconnecting.
|
||||
@@ -501,7 +497,8 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
if !idealNodeInRegion && reg != nil {
|
||||
// This is purely informative for now (2024-07-06) for stats:
|
||||
req.Header.Set(derp.IdealNodeHeader, reg.Nodes[0].Name)
|
||||
const IdealNodeHeader = "Ideal-Node"
|
||||
req.Header.Set(IdealNodeHeader, reg.Nodes[0].Name)
|
||||
// TODO(bradfitz,raggi): start a time.AfterFunc for 30m-1h or so to
|
||||
// dialNode(reg.Nodes[0]) and see if we can even TCP connect to it. If
|
||||
// so, TLS handshake it as well (which is mixed up in this massive
|
||||
@@ -599,18 +596,6 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
hostOrIP := host
|
||||
dialer := netns.NewDialer(c.logf, c.netMon)
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
if err == nil {
|
||||
hostOrIP = ip.String()
|
||||
}
|
||||
if err != nil && netns.IsSOCKSDialer(dialer) {
|
||||
// Return an error if we're not using a dial
|
||||
// proxy that can do DNS lookups for us.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tcpConn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(hostOrIP, urlPort(c.url)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial of %v: %v", host, err)
|
||||
@@ -716,18 +701,6 @@ const dialNodeTimeout = 1500 * time.Millisecond
|
||||
// TODO(bradfitz): longer if no options remain perhaps? ... Or longer
|
||||
// overall but have dialRegion start overlapping races?
|
||||
func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, error) {
|
||||
// First see if we need to use an HTTP proxy.
|
||||
proxyReq := &http.Request{
|
||||
Method: "GET", // doesn't really matter
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: c.tlsServerName(n),
|
||||
Path: "/", // unused
|
||||
},
|
||||
}
|
||||
if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil {
|
||||
return c.dialNodeUsingProxy(ctx, n, proxyURL)
|
||||
}
|
||||
|
||||
type res struct {
|
||||
c net.Conn
|
||||
@@ -737,8 +710,6 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
ctx, cancel := context.WithTimeout(ctx, dialNodeTimeout)
|
||||
defer cancel()
|
||||
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDERPHTTPClient, c.logf)
|
||||
|
||||
nwait := 0
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
nwait++
|
||||
@@ -810,71 +781,6 @@ func firstStr(a, b string) string {
|
||||
return b
|
||||
}
|
||||
|
||||
// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL.
|
||||
func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (_ net.Conn, err error) {
|
||||
pu := proxyURL
|
||||
var proxyConn net.Conn
|
||||
if pu.Scheme == "https" {
|
||||
var d tls.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "443")))
|
||||
} else {
|
||||
var d net.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "80")))
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && proxyConn != nil {
|
||||
// In a goroutine in case it's a *tls.Conn (that can block on Close)
|
||||
// TODO(bradfitz): track the underlying tcp.Conn and just close that instead.
|
||||
go proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
target := net.JoinHostPort(n.HostName, "443")
|
||||
|
||||
var authHeader string
|
||||
if v, err := tshttpproxy.GetAuthHeader(pu); err != nil {
|
||||
c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err)
|
||||
} else if v != "" {
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, target, authHeader); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
br := bufio.NewReader(proxyConn)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, err)
|
||||
return nil, err
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, res.Status)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid response status from HTTP proxy %s on CONNECT to %s: %v", pu, target, res.Status)
|
||||
}
|
||||
return proxyConn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(c.newContext(), "derphttp.Client.Send")
|
||||
if err != nil {
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
@@ -18,64 +14,6 @@ import (
|
||||
// following its HTTP request.
|
||||
const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
// Handler returns an http.Handler to be mounted at /derp, serving s.
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// These are installed both here and in cmd/derper. The check here
|
||||
// catches both cmd/derper run with DERP disabled (STUN only mode) as
|
||||
// well as DERP being run in tests with derphttp.Handler directly,
|
||||
// as netcheck still assumes this replies.
|
||||
switch r.URL.Path {
|
||||
case "/derp/probe", "/derp/latency-check":
|
||||
ProbeHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
log.Printf("Weird upgrade: %q", up)
|
||||
}
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := h.Hijack()
|
||||
if err != nil {
|
||||
log.Printf("Hijack failed: %v", err)
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := s.PublicKey()
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %s\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey.UntypedHexString())
|
||||
}
|
||||
|
||||
if v := r.Header.Get(derp.IdealNodeHeader); v != "" {
|
||||
ctx = derp.IdealNodeContextKey.WithValue(ctx, v)
|
||||
}
|
||||
|
||||
s.Accept(ctx, netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
// ProbeHandler is the endpoint that clients without UDP access (including js/wasm) hit to measure
|
||||
// DERP latency, as a replacement for UDP STUN queries.
|
||||
func ProbeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by "stringer -type=dropReason -trimprefix=dropReason"; DO NOT EDIT.
|
||||
|
||||
package derp
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[dropReasonUnknownDest-0]
|
||||
_ = x[dropReasonUnknownDestOnFwd-1]
|
||||
_ = x[dropReasonGoneDisconnected-2]
|
||||
_ = x[dropReasonQueueHead-3]
|
||||
_ = x[dropReasonQueueTail-4]
|
||||
_ = x[dropReasonWriteError-5]
|
||||
_ = x[dropReasonDupClient-6]
|
||||
_ = x[numDropReasons-7]
|
||||
}
|
||||
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClientnumDropReasons"
|
||||
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80, 94}
|
||||
|
||||
func (i dropReason) String() string {
|
||||
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
|
||||
return "dropReason(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _dropReason_name[_dropReason_index[i]:_dropReason_index[i+1]]
|
||||
}
|
||||
@@ -16,14 +16,9 @@
|
||||
package envknob
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -32,10 +27,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -410,10 +403,6 @@ func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION
|
||||
// to only be used to set known predefined app types, such as Tailscale
|
||||
// Kubernetes Operator components.
|
||||
func App() string {
|
||||
a := os.Getenv("TS_INTERNAL_APP")
|
||||
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource || a == kubetypes.AppProxyGroupEgress || a == kubetypes.AppProxyGroupIngress {
|
||||
return a
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -485,162 +474,3 @@ func PanicIfAnyEnvCheckedInInit() {
|
||||
panic("envknob check of called from init function: " + string(envCheckedInInitStack))
|
||||
}
|
||||
}
|
||||
|
||||
var applyDiskConfigErr error
|
||||
|
||||
// ApplyDiskConfigError returns the most recent result of ApplyDiskConfig.
|
||||
func ApplyDiskConfigError() error { return applyDiskConfigErr }
|
||||
|
||||
// ApplyDiskConfig returns a platform-specific config file of environment
|
||||
// keys/values and applies them. On Linux and Unix operating systems, it's a
|
||||
// no-op and always returns nil. If no platform-specific config file is found,
|
||||
// it also returns nil.
|
||||
//
|
||||
// It exists primarily for Windows and macOS to make it easy to apply
|
||||
// environment variables to a running service in a way similar to modifying
|
||||
// /etc/default/tailscaled on Linux.
|
||||
//
|
||||
// On Windows, you use %ProgramData%\Tailscale\tailscaled-env.txt instead.
|
||||
//
|
||||
// On macOS, use one of:
|
||||
//
|
||||
// - /private/var/root/Library/Containers/io.tailscale.ipn.macsys.network-extension/Data/tailscaled-env.txt
|
||||
// for standalone macOS GUI builds
|
||||
// - ~/Library/Containers/io.tailscale.ipn.macos.network-extension/Data/tailscaled-env.txt
|
||||
// for App Store builds
|
||||
// - /etc/tailscale/tailscaled-env.txt for tailscaled-on-macOS (homebrew, etc)
|
||||
func ApplyDiskConfig() (err error) {
|
||||
var f *os.File
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Stash away our return error for the healthcheck package to use.
|
||||
if f != nil {
|
||||
applyDiskConfigErr = fmt.Errorf("error parsing %s: %w", f.Name(), err)
|
||||
} else {
|
||||
applyDiskConfigErr = fmt.Errorf("error applying disk config: %w", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// First try the explicitly-provided value for development testing. Not
|
||||
// useful for users to use on their own. (if they can set this, they can set
|
||||
// any environment variable anyway)
|
||||
if name := os.Getenv("TS_DEBUG_ENV_FILE"); name != "" {
|
||||
f, err = os.Open(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening explicitly configured TS_DEBUG_ENV_FILE: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
return applyKeyValueEnv(f)
|
||||
}
|
||||
|
||||
names := getPlatformEnvFiles()
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
f, err = os.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return applyKeyValueEnv(f)
|
||||
}
|
||||
|
||||
// If we have any errors, return them; if all errors are such that
|
||||
// os.IsNotExist(err) returns true, then errs is empty and we will
|
||||
// return nil.
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// getPlatformEnvFiles returns a list of paths to the current platform's
|
||||
// optional tailscaled-env.txt file. It returns an empty list if none is
|
||||
// defined for the platform.
|
||||
func getPlatformEnvFiles() []string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return []string{
|
||||
filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt"),
|
||||
}
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology {
|
||||
return []string{"/etc/tailscale/tailscaled-env.txt"}
|
||||
}
|
||||
case "darwin":
|
||||
if version.IsSandboxedMacOS() { // the two GUI variants (App Store or separate download)
|
||||
// On the App Store variant, the home directory is set
|
||||
// to something like:
|
||||
// ~/Library/Containers/io.tailscale.ipn.macos.network-extension/Data
|
||||
//
|
||||
// On the macsys (downloadable Mac GUI) variant, the
|
||||
// home directory can be unset, but we have a working
|
||||
// directory that looks like:
|
||||
// /private/var/root/Library/Containers/io.tailscale.ipn.macsys.network-extension/Data
|
||||
//
|
||||
// Try both and see if we can find the file in either
|
||||
// location.
|
||||
var candidates []string
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
candidates = append(candidates, filepath.Join(home, "tailscaled-env.txt"))
|
||||
}
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(wd, "tailscaled-env.txt"))
|
||||
}
|
||||
|
||||
return candidates
|
||||
} else {
|
||||
// Open source / homebrew variable, running tailscaled-on-macOS.
|
||||
return []string{"/etc/tailscale/tailscaled-env.txt"}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyKeyValueEnv reads key=value lines r and calls Setenv for each.
|
||||
//
|
||||
// Empty lines and lines beginning with '#' are skipped.
|
||||
//
|
||||
// Values can be double quoted, in which case they're unquoted using
|
||||
// strconv.Unquote.
|
||||
func applyKeyValueEnv(r io.Reader) error {
|
||||
bs := bufio.NewScanner(r)
|
||||
for bs.Scan() {
|
||||
line := strings.TrimSpace(bs.Text())
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, "=")
|
||||
k = strings.TrimSpace(k)
|
||||
if !ok || k == "" {
|
||||
continue
|
||||
}
|
||||
v = strings.TrimSpace(v)
|
||||
if strings.HasPrefix(v, `"`) {
|
||||
var err error
|
||||
v, err = strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value in line %q: %v", line, err)
|
||||
}
|
||||
}
|
||||
Setenv(k, v)
|
||||
}
|
||||
return bs.Err()
|
||||
}
|
||||
|
||||
// IPCVersion returns version.Long usually, unless TS_DEBUG_FAKE_IPC_VERSION is
|
||||
// set, in which it contains that value. This is only used for weird development
|
||||
// cases when testing mismatched versions and you want the client to act like it's
|
||||
// compatible with the server.
|
||||
func IPCVersion() string {
|
||||
if v := String("TS_DEBUG_FAKE_IPC_VERSION"); v != "" {
|
||||
return v
|
||||
}
|
||||
return version.Long()
|
||||
}
|
||||
|
||||
102
health/health.go
102
health/health.go
@@ -8,11 +8,9 @@ package health
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -20,15 +18,11 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/cibuild"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/usermetric"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -108,11 +102,9 @@ type Tracker struct {
|
||||
ipnWantRunning bool
|
||||
ipnWantRunningLastTrue time.Time // when ipnWantRunning last changed false -> true
|
||||
anyInterfaceUp opt.Bool // empty means unknown (assume true)
|
||||
controlHealth []string
|
||||
lastLoginErr error
|
||||
localLogConfigErr error
|
||||
tlsConnectionErrors map[string]error // map[ServerName]error
|
||||
metricHealthMessage *metrics.MultiLabelMap[metricHealthMessageLabel]
|
||||
}
|
||||
|
||||
// Subsystem is the name of a subsystem whose health can be monitored.
|
||||
@@ -252,11 +244,6 @@ func (t *Tracker) nil() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if cibuild.On() {
|
||||
stack := make([]byte, 1<<10)
|
||||
stack = stack[:runtime.Stack(stack, false)]
|
||||
fmt.Fprintf(os.Stderr, "## WARNING: (non-fatal) nil health.Tracker (being strict in CI):\n%s\n", stack)
|
||||
}
|
||||
// TODO(bradfitz): open source our "unexpected" package
|
||||
// and use it here to capture samples of stacks where
|
||||
// t is nil.
|
||||
@@ -316,33 +303,6 @@ func (w *Warnable) IsVisible(ws *warningState) bool {
|
||||
return time.Since(ws.BrokenSince) >= w.TimeToVisible
|
||||
}
|
||||
|
||||
// SetMetricsRegistry sets up the metrics for the Tracker. It takes
|
||||
// a usermetric.Registry and registers the metrics there.
|
||||
func (t *Tracker) SetMetricsRegistry(reg *usermetric.Registry) {
|
||||
if reg == nil || t.metricHealthMessage != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.metricHealthMessage = usermetric.NewMultiLabelMapWithRegistry[metricHealthMessageLabel](
|
||||
reg,
|
||||
"tailscaled_health_messages",
|
||||
"gauge",
|
||||
"Number of health messages broken down by type.",
|
||||
)
|
||||
|
||||
t.metricHealthMessage.Set(metricHealthMessageLabel{
|
||||
Type: MetricLabelWarning,
|
||||
}, expvar.Func(func() any {
|
||||
if t.nil() {
|
||||
return 0
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.updateBuiltinWarnablesLocked()
|
||||
return int64(len(t.stringsLocked()))
|
||||
}))
|
||||
}
|
||||
|
||||
// SetUnhealthy sets a warningState for the given Warnable with the provided Args, and should be
|
||||
// called when a Warnable becomes unhealthy, or its unhealthy status needs to be updated.
|
||||
// SetUnhealthy takes ownership of args. The args can be nil if no additional information is
|
||||
@@ -617,16 +577,6 @@ func (t *Tracker) updateLegacyErrorWarnableLocked(key Subsystem, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetControlHealth(problems []string) {
|
||||
if t.nil() {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.controlHealth = problems
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
// GotStreamedMapResponse notes that we got a tailcfg.MapResponse
|
||||
// message in streaming mode, even if it's just a keep-alive message.
|
||||
//
|
||||
@@ -975,22 +925,6 @@ var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
t.updateWarmingUpWarnableLocked()
|
||||
|
||||
if w, show := t.showUpdateWarnable(); show {
|
||||
t.setUnhealthyLocked(w, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: t.latestVersion.LatestVersion,
|
||||
})
|
||||
} else {
|
||||
t.setHealthyLocked(updateAvailableWarnable)
|
||||
t.setHealthyLocked(securityUpdateAvailableWarnable)
|
||||
}
|
||||
|
||||
if version.IsUnstableBuild() {
|
||||
t.setUnhealthyLocked(unstableWarnable, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
})
|
||||
}
|
||||
|
||||
if v, ok := t.anyInterfaceUp.Get(); ok && !v {
|
||||
t.setUnhealthyLocked(NetworkStatusWarnable, nil)
|
||||
} else {
|
||||
@@ -1124,24 +1058,6 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
t.setHealthyLocked(derpRegionErrorWarnable)
|
||||
}
|
||||
|
||||
if len(t.controlHealth) > 0 {
|
||||
for _, s := range t.controlHealth {
|
||||
t.setUnhealthyLocked(controlHealthWarnable, Args{
|
||||
ArgError: s,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
t.setHealthyLocked(controlHealthWarnable)
|
||||
}
|
||||
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
t.setUnhealthyLocked(applyDiskConfigWarnable, Args{
|
||||
ArgError: err.Error(),
|
||||
})
|
||||
} else {
|
||||
t.setHealthyLocked(applyDiskConfigWarnable)
|
||||
}
|
||||
|
||||
if len(t.tlsConnectionErrors) > 0 {
|
||||
for serverName, err := range t.tlsConnectionErrors {
|
||||
t.setUnhealthyLocked(tlsConnectionFailedWarnable, Args{
|
||||
@@ -1170,24 +1086,6 @@ func (t *Tracker) updateWarmingUpWarnableLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) showUpdateWarnable() (*Warnable, bool) {
|
||||
if !t.checkForUpdates {
|
||||
return nil, false
|
||||
}
|
||||
cv := t.latestVersion
|
||||
if cv == nil || cv.RunningLatest || cv.LatestVersion == "" {
|
||||
return nil, false
|
||||
}
|
||||
if cv.UrgentSecurityUpdate {
|
||||
return securityUpdateAvailableWarnable, true
|
||||
}
|
||||
// Only show update warning when auto-updates are off
|
||||
if !t.applyUpdates.EqualBool(true) {
|
||||
return updateAvailableWarnable, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
|
||||
@@ -5,53 +5,13 @@ package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
/**
|
||||
This file contains definitions for the Warnables maintained within this `health` package.
|
||||
*/
|
||||
|
||||
// updateAvailableWarnable is a Warnable that warns the user that an update is available.
|
||||
var updateAvailableWarnable = Register(&Warnable{
|
||||
Code: "update-available",
|
||||
Title: "Update available",
|
||||
Severity: SeverityLow,
|
||||
Text: func(args Args) string {
|
||||
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
|
||||
return fmt.Sprintf("An update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
} else {
|
||||
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// securityUpdateAvailableWarnable is a Warnable that warns the user that an important security update is available.
|
||||
var securityUpdateAvailableWarnable = Register(&Warnable{
|
||||
Code: "security-update-available",
|
||||
Title: "Security update available",
|
||||
Severity: SeverityMedium,
|
||||
Text: func(args Args) string {
|
||||
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
|
||||
return fmt.Sprintf("A security update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
} else {
|
||||
return fmt.Sprintf("A security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// unstableWarnable is a Warnable that warns the user that they are using an unstable version of Tailscale
|
||||
// so they won't be surprised by all the issues that may arise.
|
||||
var unstableWarnable = Register(&Warnable{
|
||||
Code: "is-using-unstable-version",
|
||||
Title: "Using an unstable version",
|
||||
Severity: SeverityLow,
|
||||
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes. Please report any issues to Tailscale."),
|
||||
})
|
||||
|
||||
// NetworkStatusWarnable is a Warnable that warns the user that the network is down.
|
||||
var NetworkStatusWarnable = Register(&Warnable{
|
||||
Code: "network-status",
|
||||
@@ -228,26 +188,6 @@ var testWarnable = Register(&Warnable{
|
||||
},
|
||||
})
|
||||
|
||||
// applyDiskConfigWarnable is a Warnable that warns the user that there was an error applying the envknob config stored on disk.
|
||||
var applyDiskConfigWarnable = Register(&Warnable{
|
||||
Code: "apply-disk-config",
|
||||
Title: "Could not apply configuration",
|
||||
Severity: SeverityMedium,
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("An error occurred applying the Tailscale envknob configuration stored on disk: %v", args[ArgError])
|
||||
},
|
||||
})
|
||||
|
||||
// controlHealthWarnable is a Warnable that warns the user that the coordination server is reporting an health issue.
|
||||
var controlHealthWarnable = Register(&Warnable{
|
||||
Code: "control-health",
|
||||
Title: "Coordination server reports an issue",
|
||||
Severity: SeverityMedium,
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("The coordination server is reporting an health issue: %v", args[ArgError])
|
||||
},
|
||||
})
|
||||
|
||||
// warmingUpWarnableDuration is the duration for which the warmingUpWarnable is reported by the backend after the user
|
||||
// has changed ipnWantRunning to true from false.
|
||||
const warmingUpWarnableDuration = 5 * time.Second
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineiter"
|
||||
"tailscale.com/version"
|
||||
@@ -43,33 +42,24 @@ func New() *tailcfg.Hostinfo {
|
||||
OS: version.OS(),
|
||||
OSVersion: GetOSVersion(),
|
||||
Container: lazyInContainer.Get(),
|
||||
Distro: condCall(distroName),
|
||||
DistroVersion: condCall(distroVersion),
|
||||
DistroCodeName: condCall(distroCodeName),
|
||||
Env: string(GetEnvType()),
|
||||
Desktop: desktop(),
|
||||
Package: packageTypeCached(),
|
||||
GoArch: runtime.GOARCH,
|
||||
GoArchVar: lazyGoArchVar.Get(),
|
||||
GoVersion: runtime.Version(),
|
||||
Machine: condCall(unameMachine),
|
||||
DeviceModel: deviceModelCached(),
|
||||
Cloud: string(cloudenv.Get()),
|
||||
NoLogsNoSupport: envknob.NoLogsNoSupport(),
|
||||
AllowsUpdate: envknob.AllowsRemoteUpdate(),
|
||||
WoLMACs: getWoLMACs(),
|
||||
}
|
||||
}
|
||||
|
||||
// non-nil on some platforms
|
||||
var (
|
||||
osVersion func() string
|
||||
packageType func() string
|
||||
distroName func() string
|
||||
distroVersion func() string
|
||||
distroCodeName func() string
|
||||
unameMachine func() string
|
||||
deviceModel func() string
|
||||
osVersion func() string
|
||||
packageType func() string
|
||||
unameMachine func() string
|
||||
deviceModel func() string
|
||||
)
|
||||
|
||||
func condCall[T any](fn func() T) T {
|
||||
|
||||
@@ -19,9 +19,6 @@ import (
|
||||
func init() {
|
||||
osVersion = lazyOSVersion.Get
|
||||
packageType = packageTypeLinux
|
||||
distroName = distroNameLinux
|
||||
distroVersion = distroVersionLinux
|
||||
distroCodeName = distroCodeNameLinux
|
||||
deviceModel = deviceModelLinux
|
||||
}
|
||||
|
||||
|
||||
102
hostinfo/wol.go
102
hostinfo/wol.go
@@ -2,105 +2,3 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
)
|
||||
|
||||
// TODO(bradfitz): this is all too simplistic and static. It needs to run
|
||||
// continuously in response to netmon events (USB ethernet adapaters might get
|
||||
// plugged in) and look for the media type/status/etc. Right now on macOS it
|
||||
// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
|
||||
// have any media. We should only report the one that's actually connected.
|
||||
// But it works for now (2023-10-05) for fleshing out the rest.
|
||||
|
||||
var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
|
||||
|
||||
// getWoLMACs returns up to 10 MAC address of the local machine to send
|
||||
// wake-on-LAN packets to in order to wake it up. The returned MACs are in
|
||||
// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
|
||||
//
|
||||
// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
|
||||
// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
|
||||
// set to a MAC address, that sole MAC address is returned.
|
||||
func getWoLMACs() (macs []string) {
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
return nil
|
||||
}
|
||||
if s := wakeMAC(); s != "" {
|
||||
switch s {
|
||||
case "auto":
|
||||
ifs, _ := net.Interfaces()
|
||||
for _, iface := range ifs {
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if iface.Flags&net.FlagBroadcast == 0 ||
|
||||
iface.Flags&net.FlagRunning == 0 ||
|
||||
iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
if keepMAC(iface.Name, iface.HardwareAddr) {
|
||||
macs = append(macs, iface.HardwareAddr.String())
|
||||
}
|
||||
if len(macs) == 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return macs
|
||||
case "false", "off": // fast path before ParseMAC error
|
||||
return nil
|
||||
}
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
log.Printf("invalid MAC %q", s)
|
||||
return nil
|
||||
}
|
||||
return []string{mac.String()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ignoreWakeOUI = map[[3]byte]bool{
|
||||
{0x00, 0x15, 0x5d}: true, // Hyper-V
|
||||
{0x00, 0x50, 0x56}: true, // VMware
|
||||
{0x00, 0x1c, 0x14}: true, // VMware
|
||||
{0x00, 0x05, 0x69}: true, // VMware
|
||||
{0x00, 0x0c, 0x29}: true, // VMware
|
||||
{0x00, 0x1c, 0x42}: true, // Parallels
|
||||
{0x08, 0x00, 0x27}: true, // VirtualBox
|
||||
{0x00, 0x21, 0xf6}: true, // VirtualBox
|
||||
{0x00, 0x14, 0x4f}: true, // VirtualBox
|
||||
{0x00, 0x0f, 0x4b}: true, // VirtualBox
|
||||
{0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
|
||||
}
|
||||
|
||||
func keepMAC(ifName string, mac []byte) bool {
|
||||
if len(mac) != 6 {
|
||||
return false
|
||||
}
|
||||
base := strings.TrimRightFunc(ifName, unicode.IsNumber)
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
switch base {
|
||||
case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
|
||||
return false
|
||||
}
|
||||
}
|
||||
if mac[0] == 0x02 && mac[1] == 0x42 {
|
||||
// Docker container.
|
||||
return false
|
||||
}
|
||||
oui := [3]byte{mac[0], mac[1], mac[2]}
|
||||
if ignoreWakeOUI[oui] {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
type State int
|
||||
@@ -140,14 +138,6 @@ type Notify struct {
|
||||
// is available.
|
||||
ClientVersion *tailcfg.ClientVersion `json:",omitempty"`
|
||||
|
||||
// DriveShares tracks the full set of current DriveShares that we're
|
||||
// publishing. Some client applications, like the MacOS and Windows clients,
|
||||
// will listen for updates to this and handle serving these shares under
|
||||
// the identity of the unprivileged user that is running the application. A
|
||||
// nil value here means that we're not broadcasting shares information, an
|
||||
// empty value means that there are no shares.
|
||||
DriveShares views.SliceView[*drive.Share, drive.ShareView]
|
||||
|
||||
// Health is the last-known health state of the backend. When this field is
|
||||
// non-nil, a change in health verified, and the API client should surface
|
||||
// any changes to the user in the UI.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android
|
||||
//go:build !ios && !android && lanscaping_always_false
|
||||
|
||||
package conffile
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
@@ -28,16 +27,6 @@ func (src *Prefs) Clone() *Prefs {
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
dst.AdvertiseServices = append(src.AdvertiseServices[:0:0], src.AdvertiseServices...)
|
||||
if src.DriveShares != nil {
|
||||
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
|
||||
for i := range dst.DriveShares {
|
||||
if src.DriveShares[i] == nil {
|
||||
dst.DriveShares[i] = nil
|
||||
} else {
|
||||
dst.DriveShares[i] = src.DriveShares[i].Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.Persist = src.Persist.Clone()
|
||||
return dst
|
||||
}
|
||||
@@ -72,7 +61,6 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
DriveShares []*drive.Share
|
||||
AllowSingleHosts marshalAsTrueInJSON
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
@@ -97,9 +96,6 @@ func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpda
|
||||
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
|
||||
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
|
||||
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
|
||||
func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares)
|
||||
}
|
||||
func (v PrefsView) AllowSingleHosts() marshalAsTrueInJSON { return v.ж.AllowSingleHosts }
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
@@ -133,7 +129,6 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
DriveShares []*drive.Share
|
||||
AllowSingleHosts marshalAsTrueInJSON
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
@@ -6,23 +6,16 @@ package ipnauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not
|
||||
@@ -63,8 +56,7 @@ type ConnIdentity struct {
|
||||
notWindows bool // runtime.GOOS != "windows"
|
||||
|
||||
// Fields used when NotWindows:
|
||||
isUnixSock bool // Conn is a *net.UnixConn
|
||||
creds *peercred.Creds // or nil
|
||||
isUnixSock bool // Conn is a *net.UnixConn
|
||||
|
||||
// Used on Windows:
|
||||
// TODO(bradfitz): merge these into the peercreds package and
|
||||
@@ -87,19 +79,11 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
||||
return uid
|
||||
}
|
||||
}
|
||||
// For Linux tests running as Windows:
|
||||
const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet
|
||||
if ci.creds != nil && !isBroken {
|
||||
if uid, ok := ci.creds.UserID(); ok {
|
||||
return ipn.WindowsUserID(uid)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ci *ConnIdentity) Pid() int { return ci.pid }
|
||||
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
|
||||
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
|
||||
func (ci *ConnIdentity) Pid() int { return ci.pid }
|
||||
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
|
||||
|
||||
var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround")
|
||||
|
||||
@@ -148,62 +132,9 @@ func LookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
||||
//
|
||||
// TODO(bradfitz): rename it? Also make Windows use this.
|
||||
func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
return false
|
||||
}
|
||||
const ro = true
|
||||
const rw = false
|
||||
if !safesocket.PlatformUsesPeerCreds() {
|
||||
return rw
|
||||
}
|
||||
creds := ci.creds
|
||||
if creds == nil {
|
||||
logf("connection from unknown peer; read-only")
|
||||
return ro
|
||||
}
|
||||
uid, ok := creds.UserID()
|
||||
if !ok {
|
||||
logf("connection from peer with unknown userid; read-only")
|
||||
return ro
|
||||
}
|
||||
if uid == "0" {
|
||||
logf("connection from userid %v; root has access", uid)
|
||||
return rw
|
||||
}
|
||||
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||
return rw
|
||||
}
|
||||
if operatorUID != "" && uid == operatorUID {
|
||||
logf("connection from userid %v; is configured operator", uid)
|
||||
return rw
|
||||
}
|
||||
if yes, err := isLocalAdmin(uid); err != nil {
|
||||
logf("connection from userid %v; read-only; %v", uid, err)
|
||||
return ro
|
||||
} else if yes {
|
||||
logf("connection from userid %v; is local admin, has access", uid)
|
||||
return rw
|
||||
}
|
||||
logf("connection from userid %v; read-only", uid)
|
||||
return ro
|
||||
return false
|
||||
}
|
||||
|
||||
func isLocalAdmin(uid string) (bool, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var adminGroup string
|
||||
switch {
|
||||
case runtime.GOOS == "darwin":
|
||||
adminGroup = "admin"
|
||||
case distro.Get() == distro.QNAP:
|
||||
adminGroup = "administrators"
|
||||
default:
|
||||
return false, fmt.Errorf("no system admin group found")
|
||||
}
|
||||
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package ipnauth
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -18,9 +17,6 @@ import (
|
||||
func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci = &ConnIdentity{conn: c, notWindows: true}
|
||||
_, ci.isUnixSock = c.(*net.UnixConn)
|
||||
if ci.creds, _ = peercred.Get(c); ci.creds != nil {
|
||||
ci.pid, _ = ci.creds.PID()
|
||||
}
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || windows
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) stopOfflineAutoUpdate() {
|
||||
if b.offlineAutoUpdateCancel != nil {
|
||||
b.logf("offline auto-update: stopping update checks")
|
||||
b.offlineAutoUpdateCancel()
|
||||
b.offlineAutoUpdateCancel = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
|
||||
if !prefs.AutoUpdate().Apply.EqualBool(true) {
|
||||
return
|
||||
}
|
||||
// AutoUpdate.Apply field in prefs can only be true for platforms that
|
||||
// support auto-updates. But check it here again, just in case.
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return
|
||||
}
|
||||
// On macsys, auto-updates are managed by Sparkle.
|
||||
if version.IsMacSysExt() {
|
||||
return
|
||||
}
|
||||
|
||||
if b.offlineAutoUpdateCancel != nil {
|
||||
// Already running.
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
b.offlineAutoUpdateCancel = cancel
|
||||
|
||||
b.logf("offline auto-update: starting update checks")
|
||||
go b.offlineAutoUpdate(ctx)
|
||||
}
|
||||
|
||||
const offlineAutoUpdateCheckPeriod = time.Hour
|
||||
|
||||
func (b *LocalBackend) offlineAutoUpdate(ctx context.Context) {
|
||||
t := time.NewTicker(offlineAutoUpdateCheckPeriod)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
if err := b.startAutoUpdate("offline auto-update"); err != nil {
|
||||
b.logf("offline auto-update: failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,6 @@ func isNotableNotify(n *ipn.Notify) bool {
|
||||
n.Prefs != nil ||
|
||||
n.ErrMessage != nil ||
|
||||
n.LoginFinished != nil ||
|
||||
!n.DriveShares.IsNil() ||
|
||||
n.Health != nil ||
|
||||
len(n.IncomingFiles) > 0 ||
|
||||
len(n.OutgoingFiles) > 0 ||
|
||||
|
||||
@@ -4,35 +4,25 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/posture"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -53,28 +43,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
|
||||
req("/debug/pprof/heap"): handleC2NPprof,
|
||||
req("/debug/pprof/allocs"): handleC2NPprof,
|
||||
|
||||
req("POST /logtail/flush"): handleC2NLogtailFlush,
|
||||
req("POST /sockstats"): handleC2NSockStats,
|
||||
|
||||
// Check TLS certificate status.
|
||||
req("GET /tls-cert-status"): handleC2NTLSCertStatus,
|
||||
|
||||
// SSH
|
||||
req("/ssh/usernames"): handleC2NSSHUsernames,
|
||||
|
||||
// Auto-updates.
|
||||
req("GET /update"): handleC2NUpdateGet,
|
||||
req("POST /update"): handleC2NUpdatePost,
|
||||
|
||||
// Wake-on-LAN.
|
||||
req("POST /wol"): handleC2NWoL,
|
||||
|
||||
// Device posture.
|
||||
req("GET /posture/identity"): handleC2NPostureIdentityGet,
|
||||
|
||||
// App Connectors.
|
||||
req("GET /appconnector/routes"): handleC2NAppConnectorDomainRoutesGet,
|
||||
|
||||
// Linux netfilter.
|
||||
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
|
||||
|
||||
@@ -135,14 +103,6 @@ func handleC2NEcho(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
func handleC2NLogtailFlush(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
if b.TryFlushLogs() {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func handleC2NDebugGoroutines(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
@@ -199,53 +159,6 @@ func handleC2NPprof(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
c2nPprof(w, r, profile)
|
||||
}
|
||||
|
||||
func handleC2NSSHUsernames(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
res, err := b.getSSHUsernames(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
writeJSON(w, res)
|
||||
}
|
||||
|
||||
func handleC2NSockStats(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if b.sockstatLogger == nil {
|
||||
http.Error(w, "no sockstatLogger", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
b.sockstatLogger.Flush()
|
||||
fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID())
|
||||
fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo())
|
||||
}
|
||||
|
||||
// handleC2NAppConnectorDomainRoutesGet handles returning the domains
|
||||
// that the app connector is responsible for, as well as the resolved
|
||||
// IP addresses for each domain. If the node is not configured as
|
||||
// an app connector, an empty map is returned.
|
||||
func handleC2NAppConnectorDomainRoutesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /appconnector/routes received")
|
||||
|
||||
var res tailcfg.C2NAppConnectorDomainRoutesResponse
|
||||
if b.appConnector == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
return
|
||||
}
|
||||
|
||||
res.Domains = b.appConnector.DomainRoutes()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: POST /netfilter-kind received")
|
||||
|
||||
@@ -282,127 +195,6 @@ func handleC2NVIPServicesGet(b *LocalBackend, w http.ResponseWriter, r *http.Req
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /update received")
|
||||
|
||||
res := b.newC2NUpdateResponse()
|
||||
res.Started = b.c2nUpdateStarted()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func handleC2NUpdatePost(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: POST /update received")
|
||||
res := b.newC2NUpdateResponse()
|
||||
defer func() {
|
||||
if res.Err != "" {
|
||||
b.logf("c2n: POST /update failed: %s", res.Err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}()
|
||||
|
||||
if !res.Enabled {
|
||||
res.Err = "not enabled"
|
||||
return
|
||||
}
|
||||
if !res.Supported {
|
||||
res.Err = "not supported"
|
||||
return
|
||||
}
|
||||
|
||||
// Do not update if we have active inbound SSH connections. Control can set
|
||||
// force=true query parameter to override this.
|
||||
if r.FormValue("force") != "true" && b.sshServer != nil && b.sshServer.NumActiveConns() > 0 {
|
||||
res.Err = "not updating due to active SSH connections"
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.startAutoUpdate("c2n"); err != nil {
|
||||
res.Err = err.Error()
|
||||
return
|
||||
}
|
||||
res.Started = true
|
||||
}
|
||||
|
||||
func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /posture/identity received")
|
||||
|
||||
res := tailcfg.C2NPostureIdentityResponse{}
|
||||
|
||||
// Only collect posture identity if enabled on the client,
|
||||
// this will first check syspolicy, MDM settings like Registry
|
||||
// on Windows or defaults on macOS. If they are not set, it falls
|
||||
// back to the cli-flag, `--posture-checking`.
|
||||
choice, err := syspolicy.GetPreferenceOption(syspolicy.PostureChecking)
|
||||
if err != nil {
|
||||
b.logf(
|
||||
"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",
|
||||
b.Prefs().PostureChecking(),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
|
||||
res.SerialNumbers, err = posture.GetSerialNumbers(b.logf)
|
||||
if err != nil {
|
||||
b.logf("c2n: GetSerialNumbers returned error: %v", err)
|
||||
}
|
||||
|
||||
// TODO(tailscale/corp#21371, 2024-07-10): once this has landed in a stable release
|
||||
// and looks good in client metrics, remove this parameter and always report MAC
|
||||
// addresses.
|
||||
if r.FormValue("hwaddrs") == "true" {
|
||||
res.IfaceHardwareAddrs, err = posture.GetHardwareAddrs()
|
||||
if err != nil {
|
||||
b.logf("c2n: GetHardwareAddrs returned error: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.PostureDisabled = true
|
||||
}
|
||||
|
||||
b.logf("c2n: posture identity disabled=%v reported %d serials %d hwaddrs", res.PostureDisabled, len(res.SerialNumbers), len(res.IfaceHardwareAddrs))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
// If NewUpdater does not return an error, we can update the installation.
|
||||
//
|
||||
// Note that we create the Updater solely to check for errors; we do not
|
||||
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
|
||||
prefs := b.Prefs().AutoUpdate()
|
||||
return tailcfg.C2NUpdateResponse{
|
||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true),
|
||||
Supported: clientupdate.CanAutoUpdate() && !version.IsMacSysExt(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) c2nUpdateStarted() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.c2nUpdateStatus.started
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setC2NUpdateStarted(v bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.c2nUpdateStatus.started = v
|
||||
}
|
||||
|
||||
func (b *LocalBackend) trySetC2NUpdateStarted() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.c2nUpdateStatus.started {
|
||||
return false
|
||||
}
|
||||
b.c2nUpdateStatus.started = true
|
||||
return true
|
||||
}
|
||||
|
||||
// findCmdTailscale looks for the cmd/tailscale that corresponds to the
|
||||
// currently running cmd/tailscaled. It's up to the caller to verify that the
|
||||
// two match, but this function does its best to find the right one. Notably, it
|
||||
@@ -502,103 +294,3 @@ func regularFileExists(path string) bool {
|
||||
fi, err := os.Stat(path)
|
||||
return err == nil && fi.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var macs []net.HardwareAddr
|
||||
for _, macStr := range r.Form["mac"] {
|
||||
mac, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
macs = append(macs, mac)
|
||||
}
|
||||
var res struct {
|
||||
SentTo []string
|
||||
Errors []string
|
||||
}
|
||||
st := b.sys.NetMon.Get().InterfaceState()
|
||||
if st == nil {
|
||||
res.Errors = append(res.Errors, "no interface state")
|
||||
writeJSON(w, &res)
|
||||
return
|
||||
}
|
||||
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
|
||||
for _, mac := range macs {
|
||||
for ifName, ips := range st.InterfaceIPs {
|
||||
for _, ip := range ips {
|
||||
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
|
||||
continue
|
||||
}
|
||||
local := &net.UDPAddr{
|
||||
IP: ip.Addr().AsSlice(),
|
||||
Port: 0,
|
||||
}
|
||||
remote := &net.UDPAddr{
|
||||
IP: net.IPv4bcast,
|
||||
Port: 0,
|
||||
}
|
||||
if err := wol.Wake(mac, password, local, remote); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
} else {
|
||||
res.SentTo = append(res.SentTo, ifName)
|
||||
}
|
||||
break // one per interface is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(res.SentTo)
|
||||
writeJSON(w, &res)
|
||||
}
|
||||
|
||||
// handleC2NTLSCertStatus returns info about the last TLS certificate issued for the
|
||||
// provided domain. This can be called by the controlplane to clean up DNS TXT
|
||||
// records when they're no longer needed by LetsEncrypt.
|
||||
//
|
||||
// It does not kick off a cert fetch or async refresh. It only reports anything
|
||||
// that's already sitting on disk, and only reports metadata about the public
|
||||
// cert (stuff that'd be the in CT logs anyway).
|
||||
func handleC2NTLSCertStatus(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
cs, err := b.getCertStore()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
domain := r.FormValue("domain")
|
||||
if domain == "" {
|
||||
http.Error(w, "no 'domain'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ret := &tailcfg.C2NTLSCertInfo{}
|
||||
pair, err := getCertPEMCached(cs, domain, b.clock.Now())
|
||||
ret.Valid = err == nil
|
||||
if err != nil {
|
||||
ret.Error = err.Error()
|
||||
if errors.Is(err, errCertExpired) {
|
||||
ret.Expired = true
|
||||
} else if errors.Is(err, ipn.ErrStateNotExist) {
|
||||
ret.Missing = true
|
||||
ret.Error = "no certificate"
|
||||
}
|
||||
} else {
|
||||
block, _ := pem.Decode(pair.CertPEM)
|
||||
if block == nil {
|
||||
ret.Error = "invalid PEM"
|
||||
ret.Valid = false
|
||||
} else {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
ret.Error = fmt.Sprintf("invalid certificate: %v", err)
|
||||
ret.Valid = false
|
||||
} else {
|
||||
ret.NotBefore = cert.NotBefore.UTC().Format(time.RFC3339)
|
||||
ret.NotAfter = cert.NotAfter.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, ret)
|
||||
}
|
||||
|
||||
@@ -4,42 +4,3 @@
|
||||
//go:build !js && !wasm
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
c2nLogHeap = func(w http.ResponseWriter, r *http.Request) {
|
||||
// Support same optional gc parameter as net/http/pprof:
|
||||
if gc, _ := strconv.Atoi(r.FormValue("gc")); gc > 0 {
|
||||
runtime.GC()
|
||||
}
|
||||
pprof.WriteHeapProfile(w)
|
||||
}
|
||||
|
||||
c2nPprof = func(w http.ResponseWriter, r *http.Request, profile string) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
p := pprof.Lookup(string(profile))
|
||||
if p == nil {
|
||||
http.Error(w, "Unknown profile", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
gc, _ := strconv.Atoi(r.FormValue("gc"))
|
||||
if profile == "heap" && gc > 0 {
|
||||
runtime.GC()
|
||||
}
|
||||
debug, _ := strconv.Atoi(r.FormValue("debug"))
|
||||
if debug != 0 {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, profile))
|
||||
}
|
||||
p.WriteTo(w, debug)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,731 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
randv2 "math/rand/v2"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/golang-x-crypto/acme"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// Process-wide cache. (A new *Handler is created per connection,
|
||||
// effectively per request)
|
||||
var (
|
||||
// acmeMu guards all ACME operations, so concurrent requests
|
||||
// for certs don't slam ACME. The first will go through and
|
||||
// populate the on-disk cache and the rest should use that.
|
||||
acmeMu sync.Mutex
|
||||
|
||||
renewMu sync.Mutex // lock order: acmeMu before renewMu
|
||||
renewCertAt = map[string]time.Time{}
|
||||
)
|
||||
|
||||
// certDir returns (creating if needed) the directory in which cached
|
||||
// cert keypairs are stored.
|
||||
func (b *LocalBackend) certDir() (string, error) {
|
||||
d := b.TailscaleVarRoot()
|
||||
|
||||
// As a workaround for Synology DSM6 not having a "var" directory, use the
|
||||
// app's "etc" directory (on a small partition) to hold certs at least.
|
||||
// See https://github.com/tailscale/tailscale/issues/4060#issuecomment-1186592251
|
||||
if d == "" && runtime.GOOS == "linux" && distro.Get() == distro.Synology && distro.DSMVersion() == 6 {
|
||||
d = "/var/packages/Tailscale/etc" // base; we append "certs" below
|
||||
}
|
||||
if d == "" {
|
||||
return "", errors.New("no TailscaleVarRoot")
|
||||
}
|
||||
full := filepath.Join(d, "certs")
|
||||
if err := os.MkdirAll(full, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return full, nil
|
||||
}
|
||||
|
||||
var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
|
||||
|
||||
// GetCertPEM gets the TLSCertKeyPair for domain, either from cache or via the
|
||||
// ACME process. ACME process is used for new domain certs, existing expired
|
||||
// certs or existing certs that should get renewed due to upcoming expiry.
|
||||
//
|
||||
// If a cert is expired, it will be renewed synchronously otherwise it will be
|
||||
// renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
return b.GetCertPEMWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// GetCertPEMWithValidity gets the TLSCertKeyPair for domain, either from cache
|
||||
// or via the ACME process. ACME process is used for new domain certs, existing
|
||||
// expired certs or existing certs that should get renewed sooner than
|
||||
// minValidity.
|
||||
//
|
||||
// If a cert is expired, or expires sooner than minValidity, it will be renewed
|
||||
// synchronously. Otherwise it will be renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
logf := logger.WithPrefix(b.logf, fmt.Sprintf("cert(%q): ", domain))
|
||||
now := b.clock.Now()
|
||||
traceACME := func(v any) {
|
||||
if !acmeDebug() {
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", "\t")
|
||||
log.Printf("acme %T: %s", v, j)
|
||||
}
|
||||
|
||||
cs, err := b.getCertStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// If we got here, we have a valid unexpired cert.
|
||||
// Check whether we should start an async renewal.
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
// Renewal check failed, but the current cert is valid and not
|
||||
// expired, so it's safe to return.
|
||||
return pair, nil
|
||||
}
|
||||
if !shouldRenew {
|
||||
return pair, nil
|
||||
}
|
||||
if minValidity == 0 {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background, return current valid cert.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now, minValidity)
|
||||
return pair, nil
|
||||
}
|
||||
// If the caller requested a specific validity duration, fall through
|
||||
// to synchronous renewal to fulfill that.
|
||||
logf("starting sync renewal")
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now, minValidity)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
// shouldStartDomainRenewal reports whether the domain's cert should be renewed
|
||||
// based on the current time, the cert's expiry, and the ARI check.
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair, minValidity time.Duration) (bool, error) {
|
||||
if minValidity != 0 {
|
||||
cert, err := pair.parseCertificate()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
return cert.NotAfter.Sub(now) < minValidity, nil
|
||||
}
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
if renewAt, ok := renewCertAt[domain]; ok {
|
||||
return now.After(renewAt), nil
|
||||
}
|
||||
|
||||
renewTime, err := b.domainRenewalTimeByARI(cs, pair)
|
||||
if err != nil {
|
||||
// Log any ARI failure and fall back to checking for renewal by expiry.
|
||||
b.logf("acme: ARI check failed: %v; falling back to expiry-based check", err)
|
||||
renewTime, err = b.domainRenewalTimeByExpiry(pair)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
renewCertAt[domain] = renewTime
|
||||
return now.After(renewTime), nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) domainRenewed(domain string) {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
delete(renewCertAt, domain)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) domainRenewalTimeByExpiry(pair *TLSCertKeyPair) (time.Time, error) {
|
||||
cert, err := pair.parseCertificate()
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
|
||||
certLifetime := cert.NotAfter.Sub(cert.NotBefore)
|
||||
if certLifetime < 0 {
|
||||
return time.Time{}, fmt.Errorf("negative certificate lifetime %v", certLifetime)
|
||||
}
|
||||
|
||||
// Per https://github.com/tailscale/tailscale/issues/8204, check
|
||||
// whether we're more than 2/3 of the way through the certificate's
|
||||
// lifetime, which is the officially-recommended best practice by Let's
|
||||
// Encrypt.
|
||||
renewalDuration := certLifetime * 2 / 3
|
||||
renewAt := cert.NotBefore.Add(renewalDuration)
|
||||
return renewAt, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) domainRenewalTimeByARI(cs certStore, pair *TLSCertKeyPair) (time.Time, error) {
|
||||
var blocks []*pem.Block
|
||||
rest := pair.CertPEM
|
||||
for len(rest) > 0 {
|
||||
var block *pem.Block
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
return time.Time{}, fmt.Errorf("parsing certificate PEM")
|
||||
}
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
if len(blocks) < 1 {
|
||||
return time.Time{}, fmt.Errorf("could not parse certificate chain from certStore, got %d PEM block(s)", len(blocks))
|
||||
}
|
||||
ac, err := acmeClient(cs)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
ri, err := ac.FetchRenewalInfo(ctx, blocks[0].Bytes)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed to fetch renewal info from ACME server: %w", err)
|
||||
}
|
||||
if acmeDebug() {
|
||||
b.logf("acme: ARI response: %+v", ri)
|
||||
}
|
||||
|
||||
// Select a random time in the suggested window and renew if that time has
|
||||
// passed. Time is randomized per recommendation in
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
|
||||
start, end := ri.SuggestedWindow.Start, ri.SuggestedWindow.End
|
||||
renewTime := start.Add(randv2.N(end.Sub(start)))
|
||||
return renewTime, nil
|
||||
}
|
||||
|
||||
// certStore provides a way to perist and retrieve TLS certificates.
|
||||
// As of 2023-02-01, we use store certs in directories on disk everywhere
|
||||
// except on Kubernetes, where we use the state store.
|
||||
type certStore interface {
|
||||
// Read returns the cert and key for domain, if they exist and are valid
|
||||
// for now. If they're expired, it returns errCertExpired.
|
||||
// If they don't exist, it returns ipn.ErrStateNotExist.
|
||||
Read(domain string, now time.Time) (*TLSCertKeyPair, error)
|
||||
// WriteCert writes the cert for domain.
|
||||
WriteCert(domain string, cert []byte) error
|
||||
// WriteKey writes the key for domain.
|
||||
WriteKey(domain string, key []byte) error
|
||||
// ACMEKey returns the value previously stored via WriteACMEKey.
|
||||
// It is a PEM encoded ECDSA key.
|
||||
ACMEKey() ([]byte, error)
|
||||
// WriteACMEKey stores the provided PEM encoded ECDSA key.
|
||||
WriteACMEKey([]byte) error
|
||||
}
|
||||
|
||||
var errCertExpired = errors.New("cert expired")
|
||||
|
||||
var testX509Roots *x509.CertPool // set non-nil by tests
|
||||
|
||||
func (b *LocalBackend) getCertStore() (certStore, error) {
|
||||
switch b.store.(type) {
|
||||
case *store.FileStore:
|
||||
case *mem.Store:
|
||||
default:
|
||||
if hostinfo.GetEnvType() == hostinfo.Kubernetes {
|
||||
// We're running in Kubernetes with a custom StateStore,
|
||||
// use that instead of the cert directory.
|
||||
// TODO(maisem): expand this to other environments?
|
||||
return certStateStore{StateStore: b.store}, nil
|
||||
}
|
||||
}
|
||||
dir, err := b.certDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if testX509Roots != nil && !testenv.InTest() {
|
||||
panic("use of test hook outside of tests")
|
||||
}
|
||||
return certFileStore{dir: dir, testRoots: testX509Roots}, nil
|
||||
}
|
||||
|
||||
// certFileStore implements certStore by storing the cert & key files in the named directory.
|
||||
type certFileStore struct {
|
||||
dir string
|
||||
|
||||
// This field allows a test to override the CA root(s) for certificate
|
||||
// verification. If nil the default system pool is used.
|
||||
testRoots *x509.CertPool
|
||||
}
|
||||
|
||||
const acmePEMName = "acme-account.key.pem"
|
||||
|
||||
func (f certFileStore) ACMEKey() ([]byte, error) {
|
||||
pemName := filepath.Join(f.dir, acmePEMName)
|
||||
v, err := os.ReadFile(pemName)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteACMEKey(b []byte) error {
|
||||
pemName := filepath.Join(f.dir, acmePEMName)
|
||||
return atomicfile.WriteFile(pemName, b, 0600)
|
||||
}
|
||||
|
||||
func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
certPEM, err := os.ReadFile(certFile(f.dir, domain))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
keyPEM, err := os.ReadFile(keyFile(f.dir, domain))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !validCertPEM(domain, keyPEM, certPEM, f.testRoots, now) {
|
||||
return nil, errCertExpired
|
||||
}
|
||||
return &TLSCertKeyPair{CertPEM: certPEM, KeyPEM: keyPEM, Cached: true}, nil
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteCert(domain string, cert []byte) error {
|
||||
return atomicfile.WriteFile(certFile(f.dir, domain), cert, 0644)
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteKey(domain string, key []byte) error {
|
||||
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||
}
|
||||
|
||||
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
|
||||
type certStateStore struct {
|
||||
ipn.StateStore
|
||||
|
||||
// This field allows a test to override the CA root(s) for certificate
|
||||
// verification. If nil the default system pool is used.
|
||||
testRoots *x509.CertPool
|
||||
}
|
||||
|
||||
func (s certStateStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
certPEM, err := s.ReadState(ipn.StateKey(domain + ".crt"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPEM, err := s.ReadState(ipn.StateKey(domain + ".key"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !validCertPEM(domain, keyPEM, certPEM, s.testRoots, now) {
|
||||
return nil, errCertExpired
|
||||
}
|
||||
return &TLSCertKeyPair{CertPEM: certPEM, KeyPEM: keyPEM, Cached: true}, nil
|
||||
}
|
||||
|
||||
func (s certStateStore) WriteCert(domain string, cert []byte) error {
|
||||
return ipn.WriteState(s.StateStore, ipn.StateKey(domain+".crt"), cert)
|
||||
}
|
||||
|
||||
func (s certStateStore) WriteKey(domain string, key []byte) error {
|
||||
return ipn.WriteState(s.StateStore, ipn.StateKey(domain+".key"), key)
|
||||
}
|
||||
|
||||
func (s certStateStore) ACMEKey() ([]byte, error) {
|
||||
return s.ReadState(ipn.StateKey(acmePEMName))
|
||||
}
|
||||
|
||||
func (s certStateStore) WriteACMEKey(key []byte) error {
|
||||
return ipn.WriteState(s.StateStore, ipn.StateKey(acmePEMName), key)
|
||||
}
|
||||
|
||||
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
|
||||
// from cache or freshly obtained.
|
||||
type TLSCertKeyPair struct {
|
||||
CertPEM []byte // public key, in PEM form
|
||||
KeyPEM []byte // private key, in PEM form
|
||||
Cached bool // whether result came from cache
|
||||
}
|
||||
|
||||
func (kp TLSCertKeyPair) parseCertificate() (*x509.Certificate, error) {
|
||||
block, _ := pem.Decode(kp.CertPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("error parsing certificate PEM")
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("PEM block is %q, not a CERTIFICATE", block.Type)
|
||||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
// getCertPEMCached returns a non-nil keyPair if a cached keypair for domain
|
||||
// exists on disk in dir that is valid at the provided now time.
|
||||
//
|
||||
// If the keypair is expired, it returns errCertExpired.
|
||||
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
|
||||
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
// Before we read files from disk using it, validate it's halfway
|
||||
// reasonable looking.
|
||||
return nil, fmt.Errorf("invalid domain %q", domain)
|
||||
}
|
||||
return cs.Read(domain, now)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
// In case this method was triggered multiple times in parallel (when
|
||||
// serving incoming requests), check whether one of the other goroutines
|
||||
// already renewed the cert before us.
|
||||
if p, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// shouldStartDomainRenewal caches its result so it's OK to call this
|
||||
// frequently.
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, p, minValidity)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if !shouldRenew {
|
||||
return p, nil
|
||||
}
|
||||
} else if !errors.Is(err, ipn.ErrStateNotExist) && !errors.Is(err, errCertExpired) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ac, err := acmeClient(cs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
switch {
|
||||
case err == nil:
|
||||
// Great, already registered.
|
||||
logf("already had ACME account.")
|
||||
case err == acme.ErrNoAccount:
|
||||
a, err = ac.Register(ctx, new(acme.Account), acme.AcceptTOS)
|
||||
if err == acme.ErrAccountAlreadyExists {
|
||||
// Potential race. Double check.
|
||||
a, err = ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme.Register: %w", err)
|
||||
}
|
||||
logf("registered ACME account.")
|
||||
traceACME(a)
|
||||
default:
|
||||
return nil, fmt.Errorf("acme.GetReg: %w", err)
|
||||
|
||||
}
|
||||
if a.Status != acme.StatusValid {
|
||||
return nil, fmt.Errorf("unexpected ACME account status %q", a.Status)
|
||||
}
|
||||
|
||||
// Before hitting LetsEncrypt, see if this is a domain that Tailscale will do DNS challenges for.
|
||||
st := b.StatusWithoutPeers()
|
||||
if err := checkCertDomain(st, domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order, err := ac.AuthorizeOrder(ctx, []acme.AuthzID{{Type: "dns", Value: domain}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceACME(order)
|
||||
|
||||
for _, aurl := range order.AuthzURLs {
|
||||
az, err := ac.GetAuthorization(ctx, aurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceACME(az)
|
||||
for _, ch := range az.Challenges {
|
||||
if ch.Type == "dns-01" {
|
||||
rec, err := ac.DNS01ChallengeRecord(ch.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := "_acme-challenge." + domain
|
||||
|
||||
// Do a best-effort lookup to see if we've already created this DNS name
|
||||
// in a previous attempt. Don't burn too much time on it, though. Worst
|
||||
// case we ask the server to create something that already exists.
|
||||
var resolver net.Resolver
|
||||
lookupCtx, lookupCancel := context.WithTimeout(ctx, 500*time.Millisecond)
|
||||
txts, _ := resolver.LookupTXT(lookupCtx, key)
|
||||
lookupCancel()
|
||||
if slices.Contains(txts, rec) {
|
||||
logf("TXT record already existed")
|
||||
} else {
|
||||
logf("starting SetDNS call...")
|
||||
err = b.SetDNS(ctx, key, rec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SetDNS %q => %q: %w", key, rec, err)
|
||||
}
|
||||
logf("did SetDNS")
|
||||
}
|
||||
|
||||
chal, err := ac.Accept(ctx, ch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Accept: %v", err)
|
||||
}
|
||||
traceACME(chal)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orderURI := order.URI
|
||||
order, err = ac.WaitOrder(ctx, orderURI)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
if oe, ok := err.(*acme.OrderError); ok {
|
||||
logf("acme: WaitOrder: OrderError status %q", oe.Status)
|
||||
} else {
|
||||
logf("acme: WaitOrder error: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
traceACME(order)
|
||||
|
||||
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var privPEM bytes.Buffer
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csr, err := certRequest(certPrivKey, domain, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logf("requesting cert...")
|
||||
der, _, err := ac.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateOrder: %v", err)
|
||||
}
|
||||
logf("got cert")
|
||||
|
||||
var certPEM bytes.Buffer
|
||||
for _, b := range der {
|
||||
pb := &pem.Block{Type: "CERTIFICATE", Bytes: b}
|
||||
if err := pem.Encode(&certPEM, pb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.domainRenewed(domain)
|
||||
|
||||
return &TLSCertKeyPair{CertPEM: certPEM.Bytes(), KeyPEM: privPEM.Bytes()}, nil
|
||||
}
|
||||
|
||||
// certRequest generates a CSR for the given common name cn and optional SANs.
|
||||
func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) {
|
||||
req := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
DNSNames: san,
|
||||
ExtraExtensions: ext,
|
||||
}
|
||||
return x509.CreateCertificateRequest(rand.Reader, req, key)
|
||||
}
|
||||
|
||||
func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
|
||||
b, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||
return pem.Encode(w, pb)
|
||||
}
|
||||
|
||||
// parsePrivateKey is a copy of x/crypto/acme's parsePrivateKey.
|
||||
//
|
||||
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
||||
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
||||
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||
//
|
||||
// Inspired by parsePrivateKey in crypto/tls/tls.go.
|
||||
func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping")
|
||||
}
|
||||
}
|
||||
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||
}
|
||||
|
||||
func acmeKey(cs certStore) (crypto.Signer, error) {
|
||||
if v, err := cs.ACMEKey(); err == nil {
|
||||
priv, _ := pem.Decode(v)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: invalid account key found in cache")
|
||||
}
|
||||
return parsePrivateKey(priv.Bytes)
|
||||
} else if !errors.Is(err, ipn.ErrStateNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pemBuf bytes.Buffer
|
||||
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cs.WriteACMEKey(pemBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func acmeClient(cs certStore) (*acme.Client, error) {
|
||||
key, err := acmeKey(cs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acmeKey: %w", err)
|
||||
}
|
||||
// Note: if we add support for additional ACME providers (other than
|
||||
// LetsEncrypt), we should make sure that they support ARI extension (see
|
||||
// shouldStartDomainRenewalARI).
|
||||
return &acme.Client{
|
||||
Key: key,
|
||||
UserAgent: "tailscaled/" + version.Long(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validCertPEM reports whether the given certificate is valid for domain at now.
|
||||
//
|
||||
// If roots != nil, it is used instead of the system root pool. This is meant
|
||||
// to support testing, and production code should pass roots == nil.
|
||||
func validCertPEM(domain string, keyPEM, certPEM []byte, roots *x509.CertPool, now time.Time) bool {
|
||||
if len(keyPEM) == 0 || len(certPEM) == 0 {
|
||||
return false
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var leaf *x509.Certificate
|
||||
intermediates := x509.NewCertPool()
|
||||
for i, certDER := range tlsCert.Certificate {
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if i == 0 {
|
||||
leaf = cert
|
||||
} else {
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
}
|
||||
if leaf == nil {
|
||||
return false
|
||||
}
|
||||
_, err = leaf.Verify(x509.VerifyOptions{
|
||||
DNSName: domain,
|
||||
CurrentTime: now,
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// validLookingCertDomain reports whether name looks like a valid domain name that
|
||||
// we might be able to get a cert for.
|
||||
//
|
||||
// It's a light check primarily for double checking before it's used
|
||||
// as part of a filesystem path. The actual validation happens in checkCertDomain.
|
||||
func validLookingCertDomain(name string) bool {
|
||||
if name == "" ||
|
||||
strings.Contains(name, "..") ||
|
||||
strings.ContainsAny(name, ":/\\\x00") ||
|
||||
!strings.Contains(name, ".") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func checkCertDomain(st *ipnstate.Status, domain string) error {
|
||||
if domain == "" {
|
||||
return errors.New("missing domain name")
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if d == domain {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(st.CertDomains) == 0 {
|
||||
return errors.New("your Tailscale account does not support getting TLS certs")
|
||||
}
|
||||
return fmt.Errorf("invalid domain %q; must be one of %q", domain, st.CertDomains)
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
const (
|
||||
// DriveLocalPort is the port on which the Taildrive listens for location
|
||||
// connections on quad 100.
|
||||
DriveLocalPort = 8080
|
||||
)
|
||||
|
||||
// DriveSharingEnabled reports whether sharing to remote nodes via Taildrive is
|
||||
// enabled. This is currently based on checking for the drive:share node
|
||||
// attribute.
|
||||
func (b *LocalBackend) DriveSharingEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.driveSharingEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveSharingEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTaildriveShare)
|
||||
}
|
||||
|
||||
// DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes
|
||||
// is enabled. This is currently based on checking for the drive:access node
|
||||
// attribute.
|
||||
func (b *LocalBackend) DriveAccessEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.driveAccessEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveAccessEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTaildriveAccess)
|
||||
}
|
||||
|
||||
// DriveSetServerAddr tells Taildrive to use the given address for connecting
|
||||
// to the drive.FileServer that's exposing local files as an unprivileged
|
||||
// user.
|
||||
func (b *LocalBackend) DriveSetServerAddr(addr string) error {
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
fs.SetFileServerAddr(addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DriveSetShare adds the given share if no share with that name exists, or
|
||||
// replaces the existing share if one with the same name already exists. To
|
||||
// avoid potential incompatibilities across file systems, share names are
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
func (b *LocalBackend) DriveSetShare(share *drive.Share) error {
|
||||
var err error
|
||||
share.Name, err = drive.NormalizeShareName(share.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.driveSetShareLocked(share)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveSetShareLocked(share *drive.Share) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
addedShare := false
|
||||
var shares []*drive.Share
|
||||
for _, existing := range existingShares.All() {
|
||||
if existing.Name() != share.Name {
|
||||
if !addedShare && existing.Name() > share.Name {
|
||||
// Add share in order
|
||||
shares = append(shares, share)
|
||||
addedShare = true
|
||||
}
|
||||
shares = append(shares, existing.AsStruct())
|
||||
}
|
||||
}
|
||||
if !addedShare {
|
||||
shares = append(shares, share)
|
||||
}
|
||||
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
// DriveRenameShare renames the share at old name to new name. To avoid
|
||||
// potential incompatibilities across file systems, the new share name is
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
// Any of the following will result in an error.
|
||||
// - no share found under old name
|
||||
// - new share name contains disallowed characters
|
||||
// - share already exists under new name
|
||||
func (b *LocalBackend) DriveRenameShare(oldName, newName string) error {
|
||||
var err error
|
||||
newName, err = drive.NormalizeShareName(newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.driveRenameShareLocked(oldName, newName)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRenameShareLocked(oldName, newName string) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
found := false
|
||||
var shares []*drive.Share
|
||||
for _, existing := range existingShares.All() {
|
||||
if existing.Name() == newName {
|
||||
return existingShares, os.ErrExist
|
||||
}
|
||||
if existing.Name() == oldName {
|
||||
share := existing.AsStruct()
|
||||
share.Name = newName
|
||||
shares = append(shares, share)
|
||||
found = true
|
||||
} else {
|
||||
shares = append(shares, existing.AsStruct())
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return existingShares, os.ErrNotExist
|
||||
}
|
||||
|
||||
slices.SortFunc(shares, drive.CompareShares)
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
// DriveRemoveShare removes the named share. Share names are forced to
|
||||
// lowercase.
|
||||
func (b *LocalBackend) DriveRemoveShare(name string) error {
|
||||
// Force all share names to lowercase to avoid potential incompatibilities
|
||||
// with clients that don't support case-sensitive filenames.
|
||||
var err error
|
||||
name, err = drive.NormalizeShareName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.driveRemoveShareLocked(name)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRemoveShareLocked(name string) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
found := false
|
||||
var shares []*drive.Share
|
||||
for _, existing := range existingShares.All() {
|
||||
if existing.Name() != name {
|
||||
shares = append(shares, existing.AsStruct())
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return existingShares, os.ErrNotExist
|
||||
}
|
||||
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveSetSharesLocked(shares []*drive.Share) error {
|
||||
prefs := b.pm.prefs.AsStruct()
|
||||
prefs.ApplyEdits(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
DriveShares: shares,
|
||||
},
|
||||
DriveSharesSet: true,
|
||||
})
|
||||
return b.pm.setPrefsNoPermCheck(prefs.View())
|
||||
}
|
||||
|
||||
// driveNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
|
||||
// about the latest list of shares, if and only if the shares have changed since
|
||||
// the last time we notified.
|
||||
func (b *LocalBackend) driveNotifyShares(shares views.SliceView[*drive.Share, drive.ShareView]) {
|
||||
b.lastNotifiedDriveSharesMu.Lock()
|
||||
defer b.lastNotifiedDriveSharesMu.Unlock()
|
||||
if b.lastNotifiedDriveShares != nil && driveShareViewsEqual(b.lastNotifiedDriveShares, shares) {
|
||||
// shares are unchanged since last notification, don't bother notifying
|
||||
return
|
||||
}
|
||||
b.lastNotifiedDriveShares = &shares
|
||||
|
||||
// Ensures shares is not nil to distinguish "no shares" from "not notifying shares"
|
||||
if shares.IsNil() {
|
||||
shares = views.SliceOfViews(make([]*drive.Share, 0))
|
||||
}
|
||||
b.send(ipn.Notify{DriveShares: shares})
|
||||
}
|
||||
|
||||
// driveNotifyCurrentSharesLocked sends an ipn.Notify if the current set of
|
||||
// shares has changed since the last notification.
|
||||
func (b *LocalBackend) driveNotifyCurrentSharesLocked() {
|
||||
var shares views.SliceView[*drive.Share, drive.ShareView]
|
||||
if b.driveSharingEnabledLocked() {
|
||||
// Only populate shares if sharing is enabled.
|
||||
shares = b.pm.prefs.DriveShares()
|
||||
}
|
||||
|
||||
// Do the below on a goroutine to avoid deadlocking on b.mu in b.send().
|
||||
go b.driveNotifyShares(shares)
|
||||
}
|
||||
|
||||
func driveShareViewsEqual(a *views.SliceView[*drive.Share, drive.ShareView], b views.SliceView[*drive.Share, drive.ShareView]) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Len() != b.Len() {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a.Len() {
|
||||
if !drive.ShareViewsEqual(a.At(i), b.At(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// DriveGetShares gets the current list of Taildrive shares, sorted by name.
|
||||
func (b *LocalBackend) DriveGetShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.pm.prefs.DriveShares()
|
||||
}
|
||||
|
||||
// updateDrivePeersLocked sets all applicable peers from the netmap as Taildrive
|
||||
// remotes.
|
||||
func (b *LocalBackend) updateDrivePeersLocked(nm *netmap.NetworkMap) {
|
||||
fs, ok := b.sys.DriveForLocal.GetOK()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var driveRemotes []*drive.Remote
|
||||
if b.driveAccessEnabledLocked() {
|
||||
// Only populate peers if access is enabled, otherwise leave blank.
|
||||
driveRemotes = b.driveRemotesFromPeers(nm)
|
||||
}
|
||||
|
||||
fs.SetRemotes(b.netMap.Domain, driveRemotes, b.newDriveTransport())
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Remote {
|
||||
driveRemotes := make([]*drive.Remote, 0, len(nm.Peers))
|
||||
for _, p := range nm.Peers {
|
||||
peerID := p.ID()
|
||||
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), taildrivePrefix[1:])
|
||||
driveRemotes = append(driveRemotes, &drive.Remote{
|
||||
Name: p.DisplayName(false),
|
||||
URL: url,
|
||||
Available: func() bool {
|
||||
// Peers are available to Taildrive if:
|
||||
// - They are online
|
||||
// - They are allowed to share at least one folder with us
|
||||
b.mu.Lock()
|
||||
latestNetMap := b.netMap
|
||||
b.mu.Unlock()
|
||||
|
||||
idx, found := slices.BinarySearchFunc(latestNetMap.Peers, peerID, func(candidate tailcfg.NodeView, id tailcfg.NodeID) int {
|
||||
return cmp.Compare(candidate.ID(), id)
|
||||
})
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
||||
peer := latestNetMap.Peers[idx]
|
||||
|
||||
// Exclude offline peers.
|
||||
// TODO(oxtoacart): for some reason, this correctly
|
||||
// catches when a node goes from offline to online,
|
||||
// but not the other way around...
|
||||
online := peer.Online()
|
||||
if online == nil || !*online {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the peer is allowed to share with us.
|
||||
addresses := peer.Addresses()
|
||||
for _, p := range addresses.All() {
|
||||
capsMap := b.PeerCaps(p.Addr())
|
||||
if capsMap.HasCapability(tailcfg.PeerCapabilityTaildriveSharer) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
return driveRemotes
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tka
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !js
|
||||
//go:build !ios && !android && !js && !ts_omit_h2c
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
@@ -493,16 +492,6 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
||||
ipn.IsLoginServerSynonym(savedPrefs.ControlURL) {
|
||||
savedPrefs.ControlURL = ""
|
||||
}
|
||||
// Before
|
||||
// https://github.com/tailscale/tailscale/pull/11814/commits/1613b18f8280c2bce786980532d012c9f0454fa2#diff-314ba0d799f70c8998940903efb541e511f352b39a9eeeae8d475c921d66c2ac
|
||||
// prefs could set AutoUpdate.Apply=true via EditPrefs or tailnet
|
||||
// auto-update defaults. After that change, such value is "invalid" and
|
||||
// cause any EditPrefs calls to fail (other than disabling auto-updates).
|
||||
//
|
||||
// Reset AutoUpdate.Apply if we detect such invalid prefs.
|
||||
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
|
||||
savedPrefs.AutoUpdate.Apply.Clear()
|
||||
}
|
||||
|
||||
return savedPrefs.View(), nil
|
||||
}
|
||||
|
||||
@@ -1,937 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/ctxkey"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeHeader = "Content-Type"
|
||||
grpcBaseContentType = "application/grpc"
|
||||
)
|
||||
|
||||
// ErrETagMismatch signals that the given
|
||||
// If-Match header does not match with the
|
||||
// current etag of a resource.
|
||||
var ErrETagMismatch = errors.New("etag mismatch")
|
||||
|
||||
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
|
||||
|
||||
type serveHTTPContext struct {
|
||||
SrcAddr netip.AddrPort
|
||||
DestPort uint16
|
||||
|
||||
// provides funnel-specific context, nil if not funneled
|
||||
Funnel *funnelFlow
|
||||
}
|
||||
|
||||
// funnelFlow represents a funneled connection initiated via IngressPeer
|
||||
// to Host.
|
||||
type funnelFlow struct {
|
||||
Host string
|
||||
IngressPeer tailcfg.NodeView
|
||||
}
|
||||
|
||||
// localListener is the state of host-level net.Listen for a specific (Tailscale IP, port)
|
||||
// combination. If there are two TailscaleIPs (v4 and v6) and three ports being served,
|
||||
// then there will be six of these active and looping in their Run method.
|
||||
//
|
||||
// This is not used in userspace-networking mode.
|
||||
//
|
||||
// localListener is used by tailscale serve (TCP only), the built-in web client and Taildrive.
|
||||
// Most serve traffic and peer traffic for the web client are intercepted by netstack.
|
||||
// This listener exists purely for connections from the machine itself, as that goes via the kernel,
|
||||
// so we need to be in the kernel's listening/routing tables.
|
||||
type localListener struct {
|
||||
b *LocalBackend
|
||||
ap netip.AddrPort
|
||||
ctx context.Context // valid while listener is desired
|
||||
cancel context.CancelFunc // for ctx, to close listener
|
||||
logf logger.Logf
|
||||
bo *backoff.Backoff // for retrying failed Listen calls
|
||||
|
||||
handler func(net.Conn) error // handler for inbound connections
|
||||
closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any
|
||||
}
|
||||
|
||||
func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &localListener{
|
||||
b: b,
|
||||
ap: ap,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logf: logf,
|
||||
|
||||
handler: func(conn net.Conn) error {
|
||||
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
|
||||
handler := b.tcpHandlerForServe(ap.Port(), srcAddr, nil)
|
||||
if handler == nil {
|
||||
b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, ap.Port())
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
return handler(conn)
|
||||
},
|
||||
bo: backoff.NewBackoff("serve-listener", logf, 30*time.Second),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Close cancels the context and closes the listener, if any.
|
||||
func (s *localListener) Close() error {
|
||||
s.cancel()
|
||||
if close, ok := s.closeListener.LoadOk(); ok {
|
||||
s.closeListener.Store(nil)
|
||||
close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts a net.Listen for the localListener's address and port.
|
||||
// If unable to listen, it retries with exponential backoff.
|
||||
// Listen is retried until the context is canceled.
|
||||
func (s *localListener) Run() {
|
||||
for {
|
||||
ip := s.ap.Addr()
|
||||
ipStr := ip.String()
|
||||
|
||||
var lc net.ListenConfig
|
||||
if initListenConfig != nil {
|
||||
// On macOS, this sets the lc.Control hook to
|
||||
// setsockopt the interface index to bind to. This is
|
||||
// required by the network sandbox to allow binding to
|
||||
// a specific interface. Without this hook, the system
|
||||
// chooses a default interface to bind to.
|
||||
if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil {
|
||||
s.logf("localListener failed to init listen config %v, backing off: %v", s.ap, err)
|
||||
s.bo.BackOff(s.ctx, err)
|
||||
continue
|
||||
}
|
||||
// On macOS (AppStore or macsys) and if we're binding to a privileged port,
|
||||
if version.IsSandboxedMacOS() && s.ap.Port() < 1024 {
|
||||
// On macOS, we need to bind to ""/all-interfaces due to
|
||||
// the network sandbox. Ideally we would only bind to the
|
||||
// Tailscale interface, but macOS errors out if we try to
|
||||
// to listen on privileged ports binding only to a specific
|
||||
// interface. (#6364)
|
||||
ipStr = ""
|
||||
}
|
||||
}
|
||||
|
||||
tcp4or6 := "tcp4"
|
||||
if ip.Is6() {
|
||||
tcp4or6 = "tcp6"
|
||||
}
|
||||
|
||||
// while we were backing off and trying again, the context got canceled
|
||||
// so don't bind, just return, because otherwise there will be no way
|
||||
// to close this listener
|
||||
if s.ctx.Err() != nil {
|
||||
s.logf("localListener context closed before binding")
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port())))
|
||||
if err != nil {
|
||||
if s.shouldWarnAboutListenError(err) {
|
||||
s.logf("localListener failed to listen on %v, backing off: %v", s.ap, err)
|
||||
}
|
||||
s.bo.BackOff(s.ctx, err)
|
||||
continue
|
||||
}
|
||||
s.closeListener.Store(ln.Close)
|
||||
|
||||
s.logf("listening on %v", s.ap)
|
||||
err = s.handleListenersAccept(ln)
|
||||
if s.ctx.Err() != nil {
|
||||
// context canceled, we're done
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.logf("localListener accept error, retrying: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *localListener) shouldWarnAboutListenError(err error) bool {
|
||||
if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) {
|
||||
// Machine likely doesn't have IPv6 enabled (or the IP is still being
|
||||
// assigned). No need to warn. Notably, WSL2 (Issue 6303).
|
||||
return false
|
||||
}
|
||||
// TODO(bradfitz): check errors.Is(err, syscall.EADDRNOTAVAIL) etc? Let's
|
||||
// see what happens in practice.
|
||||
return true
|
||||
}
|
||||
|
||||
// handleListenersAccept accepts connections for the Listener. It calls the
|
||||
// handler in a new goroutine for each accepted connection. This is used to
|
||||
// handle local "tailscale serve" and web client traffic originating from the
|
||||
// machine itself.
|
||||
func (s *localListener) handleListenersAccept(ln net.Listener) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.handler(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// updateServeTCPPortNetMapAddrListenersLocked starts a net.Listen for configured
|
||||
// Serve ports on all the node's addresses.
|
||||
// Existing Listeners are closed if port no longer in incoming ports list.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint16) {
|
||||
// close existing listeners where port
|
||||
// is no longer in incoming ports list
|
||||
for ap, sl := range b.serveListeners {
|
||||
if !slices.Contains(ports, ap.Port()) {
|
||||
b.logf("closing listener %v", ap)
|
||||
sl.Close()
|
||||
delete(b.serveListeners, ap)
|
||||
}
|
||||
}
|
||||
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
b.logf("netMap is nil")
|
||||
return
|
||||
}
|
||||
if !nm.SelfNode.Valid() {
|
||||
b.logf("netMap SelfNode is nil")
|
||||
return
|
||||
}
|
||||
|
||||
addrs := nm.GetAddresses()
|
||||
for _, a := range addrs.All() {
|
||||
for _, p := range ports {
|
||||
addrPort := netip.AddrPortFrom(a.Addr(), p)
|
||||
if _, ok := b.serveListeners[addrPort]; ok {
|
||||
continue // already listening
|
||||
}
|
||||
|
||||
sl := b.newServeListener(context.Background(), addrPort, b.logf)
|
||||
mak.Set(&b.serveListeners, addrPort, sl)
|
||||
|
||||
go sl.Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetServeConfig establishes or replaces the current serve config.
|
||||
// ETag is an optional parameter to enforce Optimistic Concurrency Control.
|
||||
// If it is an empty string, then the config will be overwritten.
|
||||
func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig, etag string) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.setServeConfigLocked(config, etag)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string) error {
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
if config.IsFunnelOn() && prefs.ShieldsUp() {
|
||||
return errors.New("Unable to turn on Funnel while shields-up is enabled")
|
||||
}
|
||||
if b.isConfigLocked_Locked() {
|
||||
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
||||
}
|
||||
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return errors.New("netMap is nil")
|
||||
}
|
||||
if !nm.SelfNode.Valid() {
|
||||
return errors.New("netMap SelfNode is nil")
|
||||
}
|
||||
|
||||
// If etag is present, check that it has
|
||||
// not changed from the last config.
|
||||
prevConfig := b.serveConfig
|
||||
if etag != "" {
|
||||
// Note that we marshal b.serveConfig
|
||||
// and not use b.lastServeConfJSON as that might
|
||||
// be a Go nil value, which produces a different
|
||||
// checksum from a JSON "null" value.
|
||||
prevBytes, err := json.Marshal(prevConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding previous config: %w", err)
|
||||
}
|
||||
sum := sha256.Sum256(prevBytes)
|
||||
previousEtag := hex.EncodeToString(sum[:])
|
||||
if etag != previousEtag {
|
||||
return ErrETagMismatch
|
||||
}
|
||||
}
|
||||
|
||||
var bs []byte
|
||||
if config != nil {
|
||||
j, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding serve config: %w", err)
|
||||
}
|
||||
bs = j
|
||||
}
|
||||
|
||||
profileID := b.pm.CurrentProfile().ID
|
||||
confKey := ipn.ServeConfigKey(profileID)
|
||||
if err := b.store.WriteState(confKey, bs); err != nil {
|
||||
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
|
||||
}
|
||||
|
||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||
|
||||
// clean up and close all previously open foreground sessions
|
||||
// if the current ServeConfig has overwritten them.
|
||||
if prevConfig.Valid() {
|
||||
has := func(string) bool { return false }
|
||||
if b.serveConfig.Valid() {
|
||||
has = b.serveConfig.Foreground().Contains
|
||||
}
|
||||
for k := range prevConfig.Foreground().All() {
|
||||
if !has(k) {
|
||||
for _, sess := range b.notifyWatchers {
|
||||
if sess.sessionID == k {
|
||||
sess.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeConfig provides a view of the current serve mappings.
|
||||
// If serving is not configured, the returned view is not Valid.
|
||||
func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.serveConfig
|
||||
}
|
||||
|
||||
// DeleteForegroundSession deletes a ServeConfig's foreground session
|
||||
// in the LocalBackend if it exists. It also ensures check, delete, and
|
||||
// set operations happen within the same mutex lock to avoid any races.
|
||||
func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if !b.serveConfig.Valid() || !b.serveConfig.Foreground().Contains(sessionID) {
|
||||
return nil
|
||||
}
|
||||
sc := b.serveConfig.AsStruct()
|
||||
delete(sc.Foreground, sessionID)
|
||||
return b.setServeConfigLocked(sc, "")
|
||||
}
|
||||
|
||||
// HandleIngressTCPConn handles a TCP connection initiated by the ingressPeer
|
||||
// proxied to the local node over the PeerAPI.
|
||||
// Target represents the destination HostPort of the conn.
|
||||
// srcAddr represents the source AddrPort and not that of the ingressPeer.
|
||||
// getConnOrReset is a callback to get the connection, or reset if the connection
|
||||
// is no longer available.
|
||||
// sendRST is a callback to send a TCP RST to the ingressPeer indicating that
|
||||
// the connection was not accepted.
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
||||
// TODO(maisem,bradfitz): make this not alloc for every conn.
|
||||
logf := logger.WithPrefix(b.logf, "handleIngress: ")
|
||||
|
||||
if !sc.Valid() {
|
||||
logf("got ingress conn w/o serveConfig; rejecting")
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
if !sc.HasFunnelForTarget(target) {
|
||||
logf("got ingress conn for unconfigured %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(string(target))
|
||||
if err != nil {
|
||||
logf("got ingress conn for bad target %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
port16, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
logf("got ingress conn for bad target %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
dport := uint16(port16)
|
||||
if b.getTCPHandlerForFunnelFlow != nil {
|
||||
handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport)
|
||||
if handler != nil {
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
handler := b.tcpHandlerForServe(dport, srcAddr, &funnelFlow{
|
||||
Host: host,
|
||||
IngressPeer: ingressPeer,
|
||||
})
|
||||
if handler == nil {
|
||||
logf("[unexpected] no matching ingress serve handler for %v to port %v", srcAddr, dport)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
}
|
||||
|
||||
// tcpHandlerForServe returns a handler for a TCP connection to be served via
|
||||
// the ipn.ServeConfig. The funnelFlow can be nil if this is not a funneled
|
||||
// connection.
|
||||
func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort, f *funnelFlow) (handler func(net.Conn) error) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
||||
if !sc.Valid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
tcph, ok := sc.FindTCP(dport)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tcph.HTTPS() || tcph.HTTP() {
|
||||
hs := &http.Server{
|
||||
Handler: http.HandlerFunc(b.serveWebHandler),
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
|
||||
Funnel: f,
|
||||
SrcAddr: srcAddr,
|
||||
DestPort: dport,
|
||||
})
|
||||
},
|
||||
}
|
||||
if tcph.HTTPS() {
|
||||
hs.TLSConfig = &tls.Config{
|
||||
GetCertificate: b.getTLSServeCertForPort(dport),
|
||||
}
|
||||
return func(c net.Conn) error {
|
||||
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
||||
}
|
||||
}
|
||||
|
||||
return func(c net.Conn) error {
|
||||
return hs.Serve(netutil.NewOneConnListener(c, nil))
|
||||
}
|
||||
}
|
||||
|
||||
if backDst := tcph.TCPForward(); backDst != "" {
|
||||
return func(conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
||||
cancel()
|
||||
if err != nil {
|
||||
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
|
||||
return nil
|
||||
}
|
||||
defer backConn.Close()
|
||||
if sni := tcph.TerminateTLS(); sni != "" {
|
||||
conn = tls.Server(conn, &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, sni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(bradfitz): do the RegisterIPPortIdentity and
|
||||
// UnregisterIPPortIdentity stuff that netstack does
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(backConn, conn)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backConn)
|
||||
errc <- err
|
||||
}()
|
||||
return <-errc
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
|
||||
var z ipn.HTTPHandlerView // zero value
|
||||
|
||||
hostname := r.Host
|
||||
if r.TLS == nil {
|
||||
tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix
|
||||
if host, _, err := net.SplitHostPort(hostname); err == nil {
|
||||
hostname = host
|
||||
}
|
||||
if !strings.HasSuffix(hostname, tcd) {
|
||||
hostname += tcd
|
||||
}
|
||||
} else {
|
||||
hostname = r.TLS.ServerName
|
||||
}
|
||||
|
||||
sctx, ok := serveHTTPContextKey.ValueOk(r.Context())
|
||||
if !ok {
|
||||
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
|
||||
return z, "", false
|
||||
}
|
||||
wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
|
||||
if !ok {
|
||||
return z, "", false
|
||||
}
|
||||
|
||||
if h, ok := wsc.Handlers().GetOk(r.URL.Path); ok {
|
||||
return h, r.URL.Path, true
|
||||
}
|
||||
pth := path.Clean(r.URL.Path)
|
||||
for {
|
||||
withSlash := pth + "/"
|
||||
if h, ok := wsc.Handlers().GetOk(withSlash); ok {
|
||||
return h, withSlash, true
|
||||
}
|
||||
if h, ok := wsc.Handlers().GetOk(pth); ok {
|
||||
return h, pth, true
|
||||
}
|
||||
if pth == "/" {
|
||||
return z, "", false
|
||||
}
|
||||
pth = path.Dir(pth)
|
||||
}
|
||||
}
|
||||
|
||||
// proxyHandlerForBackend creates a new HTTP reverse proxy for a particular backend that
|
||||
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
|
||||
func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) {
|
||||
targetURL, insecure := expandProxyArg(backend)
|
||||
u, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||
}
|
||||
p := &reverseProxy{
|
||||
logf: b.logf,
|
||||
url: u,
|
||||
insecure: insecure,
|
||||
backend: backend,
|
||||
lb: b,
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// reverseProxy is a proxy that forwards a request to a backend host
|
||||
// (preconfigured via ipn.ServeConfig). If the host is configured with
|
||||
// http+insecure prefix, connection between proxy and backend will be over
|
||||
// insecure TLS. If the backend host has a http prefix and the incoming request
|
||||
// has application/grpc content type header, the connection will be over h2c.
|
||||
// Otherwise standard Go http transport will be used.
|
||||
type reverseProxy struct {
|
||||
logf logger.Logf
|
||||
url *url.URL
|
||||
// insecure tracks whether the connection to an https backend should be
|
||||
// insecure (i.e because we cannot verify its CA).
|
||||
insecure bool
|
||||
backend string
|
||||
lb *LocalBackend
|
||||
httpTransport lazy.SyncValue[*http.Transport] // transport for non-h2c backends
|
||||
h2cTransport lazy.SyncValue[*http2.Transport] // transport for h2c backends
|
||||
// closed tracks whether proxy is closed/currently closing.
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// close ensures that any open backend connections get closed.
|
||||
func (rp *reverseProxy) close() {
|
||||
rp.closed.Store(true)
|
||||
if h2cT := rp.h2cTransport.Get(func() *http2.Transport {
|
||||
return nil
|
||||
}); h2cT != nil {
|
||||
h2cT.CloseIdleConnections()
|
||||
}
|
||||
if httpTransport := rp.httpTransport.Get(func() *http.Transport {
|
||||
return nil
|
||||
}); httpTransport != nil {
|
||||
httpTransport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if closed := rp.closed.Load(); closed {
|
||||
rp.logf("received a request for a proxy that's being closed or has been closed")
|
||||
http.Error(w, "proxy is closed", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
p := &httputil.ReverseProxy{Rewrite: func(r *httputil.ProxyRequest) {
|
||||
oldOutPath := r.Out.URL.Path
|
||||
r.SetURL(rp.url)
|
||||
|
||||
// If mount point matches the request path exactly, the outbound
|
||||
// request URL was set to empty string in serveWebHandler which
|
||||
// would have resulted in the outbound path set to <proxy path>
|
||||
// + '/' in SetURL. In that case, if the proxy path was set, we
|
||||
// want to send the request to the <proxy path> (without the
|
||||
// '/') .
|
||||
if oldOutPath == "" && rp.url.Path != "" {
|
||||
r.Out.URL.Path = rp.url.Path
|
||||
r.Out.URL.RawPath = rp.url.RawPath
|
||||
}
|
||||
|
||||
r.Out.Host = r.In.Host
|
||||
addProxyForwardedHeaders(r)
|
||||
rp.lb.addTailscaleIdentityHeaders(r)
|
||||
}}
|
||||
|
||||
// There is no way to autodetect h2c as per RFC 9113
|
||||
// https://datatracker.ietf.org/doc/html/rfc9113#name-starting-http-2.
|
||||
// However, we assume that http:// proxy prefix in combination with the
|
||||
// protoccol being HTTP/2 is sufficient to detect h2c for our needs. Only use this for
|
||||
// gRPC to fix a known problem of plaintext gRPC backends
|
||||
if rp.shouldProxyViaH2C(r) {
|
||||
rp.logf("received a proxy request for plaintext gRPC")
|
||||
p.Transport = rp.getH2CTransport()
|
||||
} else {
|
||||
p.Transport = rp.getTransport()
|
||||
}
|
||||
p.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getTransport returns the Transport used for regular (non-GRPC) requests
|
||||
// to the backend. The Transport gets created lazily, at most once.
|
||||
func (rp *reverseProxy) getTransport() *http.Transport {
|
||||
return rp.httpTransport.Get(func() *http.Transport {
|
||||
return &http.Transport{
|
||||
DialContext: rp.lb.dialer.SystemDial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: rp.insecure,
|
||||
},
|
||||
// Values for the following parameters have been copied from http.DefaultTransport.
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getH2CTransport returns the Transport used for GRPC requests to the backend.
|
||||
// The Transport gets created lazily, at most once.
|
||||
func (rp *reverseProxy) getH2CTransport() *http2.Transport {
|
||||
return rp.h2cTransport.Get(func() *http2.Transport {
|
||||
return &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
DialTLSContext: func(ctx context.Context, network string, addr string, _ *tls.Config) (net.Conn, error) {
|
||||
return rp.lb.dialer.SystemDial(ctx, "tcp", rp.url.Host)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// This is not a generally reliable way how to determine whether a request is
|
||||
// for a h2c server, but sufficient for our particular use case.
|
||||
func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
||||
contentType := r.Header.Get(contentTypeHeader)
|
||||
return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
|
||||
}
|
||||
|
||||
// isGRPC accepts an HTTP request's content type header value and determines
|
||||
// whether this is gRPC content. grpc-go considers a value that equals
|
||||
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
|
||||
// valid grpc content type header.
|
||||
// https://github.com/grpc/grpc-go/blob/v1.60.0-dev/internal/grpcutil/method.go#L41-L78
|
||||
func isGRPCContentType(contentType string) bool {
|
||||
s, ok := strings.CutPrefix(contentType, grpcBaseContentType)
|
||||
return ok && (len(s) == 0 || s[0] == '+' || s[0] == ';')
|
||||
}
|
||||
|
||||
func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
|
||||
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
|
||||
if r.In.TLS != nil {
|
||||
r.Out.Header.Set("X-Forwarded-Proto", "https")
|
||||
}
|
||||
if c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()); ok {
|
||||
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
|
||||
// Clear any incoming values squatting in the headers.
|
||||
r.Out.Header.Del("Tailscale-User-Login")
|
||||
r.Out.Header.Del("Tailscale-User-Name")
|
||||
r.Out.Header.Del("Tailscale-User-Profile-Pic")
|
||||
r.Out.Header.Del("Tailscale-Funnel-Request")
|
||||
r.Out.Header.Del("Tailscale-Headers-Info")
|
||||
|
||||
c, ok := serveHTTPContextKey.ValueOk(r.Out.Context())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if c.Funnel != nil {
|
||||
r.Out.Header.Set("Tailscale-Funnel-Request", "?1")
|
||||
return
|
||||
}
|
||||
node, user, ok := b.WhoIs("tcp", c.SrcAddr)
|
||||
if !ok {
|
||||
return // traffic from outside of Tailnet (funneled or local machine)
|
||||
}
|
||||
if node.IsTagged() {
|
||||
// 2023-06-14: Not setting identity headers for tagged nodes.
|
||||
// Only currently set for nodes with user identities.
|
||||
return
|
||||
}
|
||||
r.Out.Header.Set("Tailscale-User-Login", encTailscaleHeaderValue(user.LoginName))
|
||||
r.Out.Header.Set("Tailscale-User-Name", encTailscaleHeaderValue(user.DisplayName))
|
||||
r.Out.Header.Set("Tailscale-User-Profile-Pic", user.ProfilePicURL)
|
||||
r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
|
||||
}
|
||||
|
||||
// encTailscaleHeaderValue cleans or encodes as necessary v, to be suitable in
|
||||
// an HTTP header value. See
|
||||
// https://github.com/tailscale/tailscale/issues/11603.
|
||||
//
|
||||
// If v is not a valid UTF-8 string, it returns an empty string.
|
||||
// If v is a valid ASCII string, it returns v unmodified.
|
||||
// If v is a valid UTF-8 string with non-ASCII characters, it returns a
|
||||
// RFC 2047 Q-encoded string.
|
||||
func encTailscaleHeaderValue(v string) string {
|
||||
if !utf8.ValidString(v) {
|
||||
return ""
|
||||
}
|
||||
return mime.QEncoding.Encode("utf-8", v)
|
||||
}
|
||||
|
||||
// serveWebHandler is an http.HandlerFunc that maps incoming requests to the
|
||||
// correct *http.
|
||||
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h, mountPoint, ok := b.getServeHandler(r)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if s := h.Text(); s != "" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
io.WriteString(w, s)
|
||||
return
|
||||
}
|
||||
if v := h.Path(); v != "" {
|
||||
b.serveFileOrDirectory(w, r, v, mountPoint)
|
||||
return
|
||||
}
|
||||
if v := h.Proxy(); v != "" {
|
||||
p, ok := b.serveProxyHandlers.Load(v)
|
||||
if !ok {
|
||||
http.Error(w, "unknown proxy destination", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h := p.(http.Handler)
|
||||
// Trim the mount point from the URL path before proxying. (#6571)
|
||||
if r.URL.Path != "/" {
|
||||
h = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), h)
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "empty handler", 500)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) serveFileOrDirectory(w http.ResponseWriter, r *http.Request, fileOrDir, mountPoint string) {
|
||||
fi, err := os.Stat(fileOrDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
b.logf("error calling stat on %s: %v", fileOrDir, err)
|
||||
http.Error(w, "an error occurred reading the file or directory", 500)
|
||||
return
|
||||
}
|
||||
if fi.Mode().IsRegular() {
|
||||
if mountPoint != r.URL.Path {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fileOrDir)
|
||||
if err != nil {
|
||||
b.logf("error opening %s: %v", fileOrDir, err)
|
||||
http.Error(w, "an error occurred reading the file or directory", 500)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
http.ServeContent(w, r, path.Base(mountPoint), fi.ModTime(), f)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
http.Error(w, "not a file or directory", 500)
|
||||
return
|
||||
}
|
||||
if len(r.URL.Path) < len(mountPoint) && r.URL.Path+"/" == mountPoint {
|
||||
http.Redirect(w, r, mountPoint, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
var fs http.Handler = http.FileServer(http.Dir(fileOrDir))
|
||||
if mountPoint != "/" {
|
||||
fs = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), fs)
|
||||
}
|
||||
fs.ServeHTTP(&fixLocationHeaderResponseWriter{
|
||||
ResponseWriter: w,
|
||||
mountPoint: mountPoint,
|
||||
}, r)
|
||||
}
|
||||
|
||||
// fixLocationHeaderResponseWriter is an http.ResponseWriter wrapper that, upon
|
||||
// flushing HTTP headers, prefixes any Location header with the mount point.
|
||||
type fixLocationHeaderResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
mountPoint string
|
||||
fixOnce sync.Once // guards call to fix
|
||||
}
|
||||
|
||||
func (w *fixLocationHeaderResponseWriter) fix() {
|
||||
h := w.ResponseWriter.Header()
|
||||
if v := h.Get("Location"); v != "" {
|
||||
h.Set("Location", w.mountPoint+v)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *fixLocationHeaderResponseWriter) WriteHeader(code int) {
|
||||
w.fixOnce.Do(w.fix)
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *fixLocationHeaderResponseWriter) Write(p []byte) (int, error) {
|
||||
w.fixOnce.Do(w.fix)
|
||||
return w.ResponseWriter.Write(p)
|
||||
}
|
||||
|
||||
// expandProxyArg returns a URL from s, where s can be of form:
|
||||
//
|
||||
// * port number ("8080")
|
||||
// * host:port ("localhost:8080")
|
||||
// * full URL ("http://localhost:8080", in which case it's returned unchanged)
|
||||
// * insecure TLS ("https+insecure://127.0.0.1:4430")
|
||||
func expandProxyArg(s string) (targetURL string, insecureSkipVerify bool) {
|
||||
if s == "" {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
|
||||
return s, false
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(s, "https+insecure://"); ok {
|
||||
return "https://" + rest, true
|
||||
}
|
||||
if allNumeric(s) {
|
||||
return "http://127.0.0.1:" + s, false
|
||||
}
|
||||
return "http://" + s, false
|
||||
}
|
||||
|
||||
func allNumeric(s string) bool {
|
||||
for i := range len(s) {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s != ""
|
||||
}
|
||||
|
||||
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if !b.serveConfig.Valid() {
|
||||
return c, false
|
||||
}
|
||||
return b.serveConfig.FindWeb(key)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
_, ok := b.webServerConfig(hi.ServerName, port)
|
||||
if !ok {
|
||||
return nil, errors.New("no webserver configured for name/port")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, hi.ServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || (darwin && !ios) || freebsd || openbsd
|
||||
//go:build (linux || (darwin && !ios) || freebsd || openbsd) && !ts_omit_ssh
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android
|
||||
//go:build !ios && !android && !ts_omit_webclient
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/ctxkey"
|
||||
"tailscale.com/util/osuser"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var _ ipnauth.Actor = (*actor)(nil)
|
||||
|
||||
// actor implements [ipnauth.Actor] and provides additional functionality that is
|
||||
// specific to the current (as of 2024-08-27) permission model.
|
||||
//
|
||||
// Deprecated: this type exists for compatibility reasons and will be removed as
|
||||
// we progress on tailscale/corp#18342.
|
||||
type actor struct {
|
||||
logf logger.Logf
|
||||
ci *ipnauth.ConnIdentity
|
||||
|
||||
clientID ipnauth.ClientID
|
||||
isLocalSystem bool // whether the actor is the Windows' Local System identity.
|
||||
}
|
||||
|
||||
func newActor(logf logger.Logf, c net.Conn) (*actor, error) {
|
||||
ci, err := ipnauth.GetConnIdentity(logf, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var clientID ipnauth.ClientID
|
||||
if pid := ci.Pid(); pid != 0 {
|
||||
// Derive [ipnauth.ClientID] from the PID of the connected client process.
|
||||
// TODO(nickkhyl): This is transient and will be re-worked as we
|
||||
// progress on tailscale/corp#18342. At minimum, we should use a 2-tuple
|
||||
// (PID + StartTime) or a 3-tuple (PID + StartTime + UID) to identify
|
||||
// the client process. This helps prevent security issues where a
|
||||
// terminated client process's PID could be reused by a different
|
||||
// process. This is not currently an issue as we allow only one user to
|
||||
// connect anyway.
|
||||
// Additionally, we should consider caching authentication results since
|
||||
// operations like retrieving a username by SID might require network
|
||||
// connectivity on domain-joined devices and/or be slow.
|
||||
clientID = ipnauth.ClientIDFrom(pid)
|
||||
}
|
||||
return &actor{logf: logf, ci: ci, clientID: clientID, isLocalSystem: connIsLocalSystem(ci)}, nil
|
||||
}
|
||||
|
||||
// IsLocalSystem implements [ipnauth.Actor].
|
||||
func (a *actor) IsLocalSystem() bool {
|
||||
return a.isLocalSystem
|
||||
}
|
||||
|
||||
// IsLocalAdmin implements [ipnauth.Actor].
|
||||
func (a *actor) IsLocalAdmin(operatorUID string) bool {
|
||||
return a.isLocalSystem || connIsLocalAdmin(a.logf, a.ci, operatorUID)
|
||||
}
|
||||
|
||||
// UserID implements [ipnauth.Actor].
|
||||
func (a *actor) UserID() ipn.WindowsUserID {
|
||||
return a.ci.WindowsUserID()
|
||||
}
|
||||
|
||||
func (a *actor) pid() int {
|
||||
return a.ci.Pid()
|
||||
}
|
||||
|
||||
// ClientID implements [ipnauth.Actor].
|
||||
func (a *actor) ClientID() (_ ipnauth.ClientID, ok bool) {
|
||||
return a.clientID, a.clientID != ipnauth.NoClientID
|
||||
}
|
||||
|
||||
// Username implements [ipnauth.Actor].
|
||||
func (a *actor) Username() (string, error) {
|
||||
if a.ci == nil {
|
||||
a.logf("[unexpected] missing ConnIdentity in ipnserver.actor")
|
||||
return "", errors.New("missing ConnIdentity")
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
tok, err := a.ci.WindowsToken()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get windows token: %w", err)
|
||||
}
|
||||
defer tok.Close()
|
||||
return tok.Username()
|
||||
case "darwin", "linux", "illumos", "solaris":
|
||||
uid, ok := a.ci.Creds().UserID()
|
||||
if !ok {
|
||||
return "", errors.New("missing user ID")
|
||||
}
|
||||
u, err := osuser.LookupByUID(uid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup user: %w", err)
|
||||
}
|
||||
return u.Username, nil
|
||||
default:
|
||||
return "", errors.New("unsupported OS")
|
||||
}
|
||||
}
|
||||
|
||||
type actorOrError struct {
|
||||
actor *actor
|
||||
err error
|
||||
}
|
||||
|
||||
func (a actorOrError) unwrap() (*actor, error) {
|
||||
return a.actor, a.err
|
||||
}
|
||||
|
||||
var errNoActor = errors.New("connection actor not available")
|
||||
|
||||
var actorKey = ctxkey.New("ipnserver.actor", actorOrError{err: errNoActor})
|
||||
|
||||
// contextWithActor returns a new context that carries the identity of the actor
|
||||
// owning the other end of the [net.Conn]. It can be retrieved with [actorFromContext].
|
||||
func contextWithActor(ctx context.Context, logf logger.Logf, c net.Conn) context.Context {
|
||||
actor, err := newActor(logf, c)
|
||||
return actorKey.WithValue(ctx, actorOrError{actor: actor, err: err})
|
||||
}
|
||||
|
||||
// actorFromContext returns an [actor] associated with ctx,
|
||||
// or an error if the context does not carry an actor's identity.
|
||||
func actorFromContext(ctx context.Context) (*actor, error) {
|
||||
return actorKey.Value(ctx).unwrap()
|
||||
}
|
||||
|
||||
func connIsLocalSystem(ci *ipnauth.ConnIdentity) bool {
|
||||
token, err := ci.WindowsToken()
|
||||
return err == nil && token.IsLocalSystem()
|
||||
}
|
||||
|
||||
// connIsLocalAdmin reports whether the connected client has administrative
|
||||
// access to the local machine, for whatever that means with respect to the
|
||||
// current OS.
|
||||
//
|
||||
// This is useful because tailscaled itself always runs with elevated rights:
|
||||
// we want to avoid privilege escalation for certain mutative operations.
|
||||
func connIsLocalAdmin(logf logger.Logf, ci *ipnauth.ConnIdentity, operatorUID string) bool {
|
||||
if ci == nil {
|
||||
logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
|
||||
return false
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
tok, err := ci.WindowsToken()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer tok.Close()
|
||||
|
||||
return tok.IsElevated()
|
||||
|
||||
case "darwin":
|
||||
// Unknown, or at least unchecked on sandboxed macOS variants. Err on
|
||||
// the side of less permissions.
|
||||
//
|
||||
// authorizeServeConfigForGOOSAndUserContext should not call
|
||||
// connIsLocalAdmin on sandboxed variants anyway.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return false
|
||||
}
|
||||
// This is a standalone tailscaled setup, use the same logic as on
|
||||
// Linux.
|
||||
fallthrough
|
||||
case "linux":
|
||||
uid, ok := ci.Creds().UserID()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// root is always admin.
|
||||
if uid == "0" {
|
||||
return true
|
||||
}
|
||||
// if non-root, must be operator AND able to execute "sudo tailscale".
|
||||
if operatorUID != "" && uid != operatorUID {
|
||||
return false
|
||||
}
|
||||
u, err := osuser.LookupByUID(uid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Short timeout just in case sudo hangs for some reason.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
)
|
||||
|
||||
// handleProxyConnectConn handles a CONNECT request to
|
||||
// log.tailscale.com (or whatever the configured log server is). This
|
||||
// is intended for use by the Windows GUI client to log via when an
|
||||
// exit node is in use, so the logs don't go out via the exit node and
|
||||
// instead go directly, like tailscaled's. The dialer tried to do that
|
||||
// in the unprivileged GUI by binding to a specific interface, but the
|
||||
// "Internet Kill Switch" installed by tailscaled for exit nodes
|
||||
// precludes that from working and instead the GUI fails to dial out.
|
||||
// So, go through tailscaled (with a CONNECT request) instead.
|
||||
func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if r.Method != "CONNECT" {
|
||||
panic("[unexpected] miswired")
|
||||
}
|
||||
|
||||
hostPort := r.RequestURI
|
||||
logHost := logpolicy.LogHost()
|
||||
allowed := net.JoinHostPort(logHost, "443")
|
||||
if hostPort != allowed {
|
||||
s.logf("invalid CONNECT target %q; want %q", hostPort, allowed)
|
||||
http.Error(w, "Bad CONNECT target.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
dialContext := logpolicy.MakeDialFunc(s.netMon, s.logf)
|
||||
back, err := dialContext(ctx, "tcp", hostPort)
|
||||
if err != nil {
|
||||
s.logf("error CONNECT dialing %v: %v", hostPort, err)
|
||||
http.Error(w, "Connect failure", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer back.Close()
|
||||
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "CONNECT hijack unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
c, br, err := hj.Hijack()
|
||||
if err != nil {
|
||||
s.logf("CONNECT hijack: %v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n")
|
||||
|
||||
errc := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(c, back)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(back, br)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
}
|
||||
@@ -8,8 +8,6 @@ package ipnserver
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,7 +16,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
@@ -27,9 +24,7 @@ import (
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/systemd"
|
||||
)
|
||||
|
||||
// Server is an IPN backend and its set of 0 or more active localhost
|
||||
@@ -50,9 +45,8 @@ type Server struct {
|
||||
// lock order: mu, then LocalBackend.mu
|
||||
mu sync.Mutex
|
||||
lastUserID ipn.WindowsUserID // tracks last userid; on change, Reset state for paranoia
|
||||
activeReqs map[*http.Request]*actor
|
||||
backendWaiter waiterSet // of LocalBackend waiters
|
||||
zeroReqWaiter waiterSet // of blockUntilZeroConnections waiters
|
||||
backendWaiter waiterSet // of LocalBackend waiters
|
||||
zeroReqWaiter waiterSet // of blockUntilZeroConnections waiters
|
||||
}
|
||||
|
||||
func (s *Server) mustBackend() *ipnlocal.LocalBackend {
|
||||
@@ -149,7 +143,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
if envknob.GOOS() == "windows" {
|
||||
// For the GUI client when using an exit node. See docs on handleProxyConnectConn.
|
||||
s.handleProxyConnectConn(w, r)
|
||||
// LANSCAPING
|
||||
} else {
|
||||
http.Error(w, "bad method for platform", http.StatusMethodNotAllowed)
|
||||
}
|
||||
@@ -171,21 +165,10 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ci, err := actorFromContext(r.Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, errNoActor) {
|
||||
http.Error(w, "internal error: "+err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
onDone, err := s.addActiveHTTPRequest(r, ci)
|
||||
onDone, err := s.addActiveHTTPRequest(r)
|
||||
if err != nil {
|
||||
if ou, ok := err.(inUseOtherUserError); ok && localapi.InUseOtherUserIPNStream(w, r, ou.Unwrap()) {
|
||||
w.(http.Flusher).Flush()
|
||||
s.blockWhileIdentityInUse(ctx, ci)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
@@ -195,9 +178,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = ci.Permissions(lb.OperatorUserID())
|
||||
lah.PermitCert = ci.CanFetchCerts()
|
||||
lah.Actor = ci
|
||||
lah.PermitRead, lah.PermitWrite = true, true
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -207,14 +188,6 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if envknob.GOOS() == "windows" {
|
||||
// TODO(bradfitz): remove this once we moved to named pipes for LocalAPI
|
||||
// on Windows. This could then move to all platforms instead at
|
||||
// 100.100.100.100 or something (quad100 handler in LocalAPI)
|
||||
s.ServeHTMLStatus(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.\n")
|
||||
}
|
||||
|
||||
@@ -230,88 +203,11 @@ func (e inUseOtherUserError) Unwrap() error { return e.error }
|
||||
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) checkConnIdentityLocked(ci *actor) error {
|
||||
// If clients are already connected, verify they're the same user.
|
||||
// This mostly matters on Windows at the moment.
|
||||
if len(s.activeReqs) > 0 {
|
||||
var active *actor
|
||||
for _, active = range s.activeReqs {
|
||||
break
|
||||
}
|
||||
if active != nil {
|
||||
// Always allow Windows SYSTEM user to connect,
|
||||
// even if Tailscale is currently being used by another user.
|
||||
if ci.IsLocalSystem() {
|
||||
return nil
|
||||
}
|
||||
func (s *Server) checkConnIdentityLocked() error {
|
||||
|
||||
if ci.UserID() != active.UserID() {
|
||||
var b strings.Builder
|
||||
b.WriteString("Tailscale already in use")
|
||||
if username, err := active.Username(); err == nil {
|
||||
fmt.Fprintf(&b, " by %s", username)
|
||||
}
|
||||
fmt.Fprintf(&b, ", pid %d", active.pid())
|
||||
return inUseOtherUserError{errors.New(b.String())}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil {
|
||||
return inUseOtherUserError{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// blockWhileIdentityInUse blocks while ci can't connect to the server because
|
||||
// the server is in use by a different user.
|
||||
//
|
||||
// This is primarily used for the Windows GUI, to block until one user's done
|
||||
// controlling the tailscaled process.
|
||||
func (s *Server) blockWhileIdentityInUse(ctx context.Context, actor *actor) error {
|
||||
inUse := func() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
_, ok := s.checkConnIdentityLocked(actor).(inUseOtherUserError)
|
||||
return ok
|
||||
}
|
||||
for inUse() {
|
||||
// Check whenever the connection count drops down to zero.
|
||||
ready, cleanup := s.zeroReqWaiter.add(&s.mu, ctx)
|
||||
<-ready
|
||||
cleanup()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Permissions returns the actor's permissions for accessing
|
||||
// the Tailscale local daemon API. The operatorUID is only used on
|
||||
// Unix-like platforms and specifies the ID of a local user
|
||||
// (in the os/user.User.Uid string form) who is allowed
|
||||
// to operate tailscaled without being root or using sudo.
|
||||
func (a *actor) Permissions(operatorUID string) (read, write bool) {
|
||||
switch envknob.GOOS() {
|
||||
case "windows":
|
||||
// As of 2024-08-27, according to the current permission model,
|
||||
// Windows users always have read/write access to the local API if
|
||||
// they're allowed to connect. Whether a user is allowed to connect
|
||||
// is determined by [Server.checkConnIdentityLocked] when adding a
|
||||
// new connection in [Server.addActiveHTTPRequest]. Therefore, it's
|
||||
// acceptable to permit read and write access without any additional
|
||||
// checks here. Note that this permission model is being changed in
|
||||
// tailscale/corp#18342.
|
||||
return true, true
|
||||
case "js":
|
||||
return true, true
|
||||
}
|
||||
if a.ci.IsUnixSock() {
|
||||
return true, !a.ci.IsReadonlyConn(operatorUID, logger.Discard)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// userIDFromString maps from either a numeric user id in string form
|
||||
// ("998") or username ("caddy") to its string userid ("998").
|
||||
// It returns the empty string on error.
|
||||
@@ -335,36 +231,13 @@ func isAllDigit(s string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CanFetchCerts reports whether the actor is allowed to fetch HTTPS
|
||||
// certs from this server when it wouldn't otherwise be able to.
|
||||
//
|
||||
// That is, this reports whether the actor should grant additional
|
||||
// capabilities over what the actor would otherwise be able to do.
|
||||
//
|
||||
// For now this only returns true on Unix machines when
|
||||
// TS_PERMIT_CERT_UID is set the to the userid of the peer
|
||||
// connection. It's intended to give your non-root webserver access
|
||||
// (www-data, caddy, nginx, etc) to certs.
|
||||
func (a *actor) CanFetchCerts() bool {
|
||||
if a.ci.IsUnixSock() && a.ci.Creds() != nil {
|
||||
connUID, ok := a.ci.Creds().UserID()
|
||||
if ok && connUID == userIDFromString(envknob.String("TS_PERMIT_CERT_UID")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// addActiveHTTPRequest adds c to the server's list of active HTTP requests.
|
||||
//
|
||||
// It returns an error if the specified actor is not allowed to connect.
|
||||
// The returned error may be of type [inUseOtherUserError].
|
||||
//
|
||||
// onDone must be called when the HTTP request is done.
|
||||
func (s *Server) addActiveHTTPRequest(req *http.Request, actor *actor) (onDone func(), err error) {
|
||||
if actor == nil {
|
||||
return nil, errors.New("internal error: nil actor")
|
||||
}
|
||||
func (s *Server) addActiveHTTPRequest(req *http.Request) (onDone func(), err error) {
|
||||
|
||||
lb := s.mustBackend()
|
||||
|
||||
@@ -381,51 +254,12 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, actor *actor) (onDone f
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := s.checkConnIdentityLocked(actor); err != nil {
|
||||
if err := s.checkConnIdentityLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mak.Set(&s.activeReqs, req, actor)
|
||||
|
||||
if len(s.activeReqs) == 1 {
|
||||
if envknob.GOOS() == "windows" && !actor.IsLocalSystem() {
|
||||
// Tell the LocalBackend about the identity we're now running as,
|
||||
// unless its the SYSTEM user. That user is not a real account and
|
||||
// doesn't have a home directory.
|
||||
uid, err := lb.SetCurrentUser(actor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.lastUserID != uid {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDone = func() {
|
||||
s.mu.Lock()
|
||||
delete(s.activeReqs, req)
|
||||
remain := len(s.activeReqs)
|
||||
s.mu.Unlock()
|
||||
|
||||
if remain == 0 && s.resetOnZero {
|
||||
if lb.InServerMode() {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
lb.ResetForClientDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Wake up callers waiting for the server to be idle:
|
||||
if remain == 0 {
|
||||
s.mu.Lock()
|
||||
s.zeroReqWaiter.wakeAll()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return onDone, nil
|
||||
@@ -437,15 +271,14 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, actor *actor) (onDone f
|
||||
//
|
||||
// At some point, either before or after Run, the Server's SetLocalBackend
|
||||
// method must also be called before Server can do anything useful.
|
||||
func New(logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) *Server {
|
||||
func New(logf logger.Logf, netMon *netmon.Monitor) *Server {
|
||||
if netMon == nil {
|
||||
panic("nil netMon")
|
||||
}
|
||||
return &Server{
|
||||
backendLogID: logID,
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
resetOnZero: envknob.GOOS() == "windows",
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
resetOnZero: envknob.GOOS() == "windows",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,15 +329,10 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||
ln.Close()
|
||||
}()
|
||||
|
||||
systemd.Ready()
|
||||
|
||||
hs := &http.Server{
|
||||
Handler: http.HandlerFunc(s.serveHTTP),
|
||||
BaseContext: func(_ net.Listener) context.Context { return ctx },
|
||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
||||
return contextWithActor(ctx, s.logf, c)
|
||||
},
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(s.logf, "ipnserver: ")),
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(s.logf, "ipnserver: ")),
|
||||
}
|
||||
if err := hs.Serve(ln); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
@@ -514,29 +342,3 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTMLStatus serves an HTML status page at http://localhost:41112/ for
|
||||
// Windows and via $DEBUG_LISTENER/debug/ipn when tailscaled's --debug flag
|
||||
// is used to run a debug server.
|
||||
func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
|
||||
lb := s.lb.Load()
|
||||
if lb == nil {
|
||||
http.Error(w, "no LocalBackend", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// As this is only meant for debug, verify there's no DNS name being used to
|
||||
// access this.
|
||||
if !strings.HasPrefix(r.Host, "localhost:") && strings.IndexFunc(r.Host, unicode.IsLetter) != -1 {
|
||||
http.Error(w, "invalid host", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := lb.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
st.WriteHTML(w)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
package ipnstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net/netip"
|
||||
"slices"
|
||||
@@ -18,16 +15,12 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAPeer
|
||||
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
// Version is the daemon's long version (see version.Long).
|
||||
@@ -87,77 +80,6 @@ type Status struct {
|
||||
ClientVersion *tailcfg.ClientVersion
|
||||
}
|
||||
|
||||
// TKAKey describes a key trusted by network lock.
|
||||
type TKAKey struct {
|
||||
Key key.NLPublic
|
||||
Metadata map[string]string
|
||||
Votes uint
|
||||
}
|
||||
|
||||
// TKAPeer describes a peer and its network lock details.
|
||||
type TKAPeer struct {
|
||||
Name string // DNS
|
||||
ID tailcfg.NodeID
|
||||
StableID tailcfg.StableNodeID
|
||||
TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node
|
||||
NodeKey key.NodePublic
|
||||
NodeKeySignature tka.NodeKeySignature
|
||||
}
|
||||
|
||||
// NetworkLockStatus represents whether network-lock is enabled,
|
||||
// along with details about the locally-known state of the tailnet
|
||||
// key authority.
|
||||
type NetworkLockStatus struct {
|
||||
// Enabled is true if network lock is enabled.
|
||||
Enabled bool
|
||||
|
||||
// Head describes the AUM hash of the leaf AUM. Head is nil
|
||||
// if network lock is not enabled.
|
||||
Head *[32]byte
|
||||
|
||||
// PublicKey describes the node's network-lock public key.
|
||||
// It may be zero if the node has not logged in.
|
||||
PublicKey key.NLPublic
|
||||
|
||||
// NodeKey describes the node's current node-key. This field is not
|
||||
// populated if the node is not operating (i.e. waiting for a login).
|
||||
NodeKey *key.NodePublic
|
||||
|
||||
// NodeKeySigned is true if our node is authorized by network-lock.
|
||||
NodeKeySigned bool
|
||||
|
||||
// NodeKeySignature is the current signature of this node's key.
|
||||
NodeKeySignature *tka.NodeKeySignature
|
||||
|
||||
// TrustedKeys describes the keys currently trusted to make changes
|
||||
// to network-lock.
|
||||
TrustedKeys []TKAKey
|
||||
|
||||
// VisiblePeers describes peers which are visible in the netmap that
|
||||
// have valid Tailnet Lock signatures signatures.
|
||||
VisiblePeers []*TKAPeer
|
||||
|
||||
// FilteredPeers describes peers which were removed from the netmap
|
||||
// (i.e. no connectivity) because they failed tailnet lock
|
||||
// checks.
|
||||
FilteredPeers []*TKAPeer
|
||||
|
||||
// StateID is a nonce associated with the network lock authority,
|
||||
// generated upon enablement. This field is not populated if the
|
||||
// network lock is disabled.
|
||||
StateID uint64
|
||||
}
|
||||
|
||||
// NetworkLockUpdate describes a change to network-lock state.
|
||||
type NetworkLockUpdate struct {
|
||||
Hash [32]byte
|
||||
Change string // values of tka.AUMKind.String()
|
||||
|
||||
// Raw contains the serialized AUM. The AUM is sent in serialized
|
||||
// form to avoid transitive dependences bloating this package.
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
// TailnetStatus is information about a Tailscale network ("tailnet").
|
||||
type TailnetStatus struct {
|
||||
// Name is the name of the network that's currently in use.
|
||||
@@ -519,143 +441,6 @@ type StatusUpdater interface {
|
||||
UpdateStatus(*StatusBuilder)
|
||||
}
|
||||
|
||||
func (st *Status) WriteHTML(w io.Writer) {
|
||||
f := func(format string, args ...any) { fmt.Fprintf(w, format, args...) }
|
||||
|
||||
f(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Tailscale State</title>
|
||||
<style>
|
||||
body { font-family: monospace; }
|
||||
.owner { text-decoration: underline; }
|
||||
.tailaddr { font-style: italic; }
|
||||
.acenter { text-align: center; }
|
||||
.aright { text-align: right; }
|
||||
table, th, td { border: 1px solid black; border-spacing : 0; border-collapse : collapse; }
|
||||
thead { background-color: #FFA500; }
|
||||
th, td { padding: 5px; }
|
||||
td { vertical-align: top; }
|
||||
table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tailscale State</h1>
|
||||
`)
|
||||
|
||||
//f("<p><b>logid:</b> %s</p>\n", logid)
|
||||
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
|
||||
|
||||
ips := make([]string, 0, len(st.TailscaleIPs))
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
|
||||
|
||||
f("<table>\n<thead>\n")
|
||||
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
|
||||
f("</thead>\n<tbody>\n")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
var peers []*PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
SortPeers(peers)
|
||||
|
||||
for _, ps := range peers {
|
||||
var actAgo string
|
||||
if !ps.LastWrite.IsZero() {
|
||||
ago := now.Sub(ps.LastWrite)
|
||||
actAgo = ago.Round(time.Second).String() + " ago"
|
||||
if ago < 5*time.Minute {
|
||||
actAgo = "<b>" + actAgo + "</b>"
|
||||
}
|
||||
}
|
||||
var owner string
|
||||
if up, ok := st.User[ps.UserID]; ok {
|
||||
owner = up.LoginName
|
||||
if i := strings.Index(owner, "@"); i != -1 {
|
||||
owner = owner[:i]
|
||||
}
|
||||
}
|
||||
|
||||
hostName := dnsname.SanitizeHostname(ps.HostName)
|
||||
dnsName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
|
||||
hostName = ""
|
||||
}
|
||||
var hostNameHTML string
|
||||
if hostName != "" {
|
||||
hostNameHTML = "<br>" + html.EscapeString(hostName)
|
||||
}
|
||||
|
||||
var tailAddr string
|
||||
if len(ps.TailscaleIPs) > 0 {
|
||||
tailAddr = ps.TailscaleIPs[0].String()
|
||||
}
|
||||
f("<tr><td>%s</td><td class=acenter>%s</td>"+
|
||||
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
ps.PublicKey.ShortString(),
|
||||
osEmoji(ps.OS),
|
||||
html.EscapeString(dnsName),
|
||||
hostNameHTML,
|
||||
tailAddr,
|
||||
html.EscapeString(owner),
|
||||
ps.RxBytes,
|
||||
ps.TxBytes,
|
||||
actAgo,
|
||||
)
|
||||
f("<td>")
|
||||
|
||||
if ps.Active {
|
||||
if ps.Relay != "" && ps.CurAddr == "" {
|
||||
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
|
||||
}
|
||||
}
|
||||
|
||||
f("</td>") // end Addrs
|
||||
|
||||
f("</tr>\n")
|
||||
}
|
||||
f("</tbody>\n</table>\n")
|
||||
f("</body>\n</html>\n")
|
||||
}
|
||||
|
||||
func osEmoji(os string) string {
|
||||
switch os {
|
||||
case "linux":
|
||||
return "🐧"
|
||||
case "macOS":
|
||||
return "🍎"
|
||||
case "windows":
|
||||
return "🖥️"
|
||||
case "iOS":
|
||||
return "📱"
|
||||
case "tvOS":
|
||||
return "🍎📺"
|
||||
case "android":
|
||||
return "🤖"
|
||||
case "freebsd":
|
||||
return "👿"
|
||||
case "openbsd":
|
||||
return "🐡"
|
||||
case "illumos":
|
||||
return "☀️"
|
||||
case "solaris":
|
||||
return "🌤️"
|
||||
}
|
||||
return "👽"
|
||||
}
|
||||
|
||||
// PingResult contains response information for the "tailscale ping" subcommand,
|
||||
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
|
||||
// See tailcfg.PingResponse for a related response that is sent back to control
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
package ipnstate
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of TKAPeer.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *TKAPeer) Clone() *TKAPeer {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(TKAPeer)
|
||||
*dst = *src
|
||||
dst.TailscaleIPs = append(src.TailscaleIPs[:0:0], src.TailscaleIPs...)
|
||||
dst.NodeKeySignature = *src.NodeKeySignature.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TKAPeerCloneNeedsRegeneration = TKAPeer(struct {
|
||||
Name string
|
||||
ID tailcfg.NodeID
|
||||
StableID tailcfg.StableNodeID
|
||||
TailscaleIPs []netip.Addr
|
||||
NodeKey key.NodePublic
|
||||
NodeKeySignature tka.NodeKeySignature
|
||||
}{})
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !js
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite && !h.PermitCert {
|
||||
http.Error(w, "cert access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
domain, ok := strings.CutPrefix(r.URL.Path, "/localapi/v0/cert/")
|
||||
if !ok {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
var minValidity time.Duration
|
||||
if minValidityStr := r.URL.Query().Get("min_validity"); minValidityStr != "" {
|
||||
var err error
|
||||
minValidity, err = time.ParseDuration(minValidityStr)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid validity parameter: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
pair, err := h.b.GetCertPEMWithValidity(r.Context(), domain, minValidity)
|
||||
if err != nil {
|
||||
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
|
||||
// GetCertPEM (and everywhere) should carry info info to get whether
|
||||
// they're 400 vs 403 vs 500 at minimum. And then we should have helpers
|
||||
// (in tsweb probably) to return an error that looks at the error value
|
||||
// to determine the HTTP status code.
|
||||
http.Error(w, fmt.Sprint(err), 500)
|
||||
return
|
||||
}
|
||||
serveKeyPair(w, r, pair)
|
||||
}
|
||||
|
||||
func serveKeyPair(w http.ResponseWriter, r *http.Request, p *ipnlocal.TLSCertKeyPair) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
switch r.URL.Query().Get("type") {
|
||||
case "", "crt", "cert":
|
||||
w.Write(p.CertPEM)
|
||||
case "key":
|
||||
w.Write(p.KeyPEM)
|
||||
case "pair":
|
||||
w.Write(p.KeyPEM)
|
||||
w.Write(p.CertPEM)
|
||||
default:
|
||||
http.Error(w, `invalid type; want "cert" (default), "key", or "pair"`, 400)
|
||||
}
|
||||
}
|
||||
@@ -2,303 +2,3 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var st ipnstate.DebugDERPRegionReport
|
||||
defer func() {
|
||||
j, _ := json.Marshal(st)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}()
|
||||
|
||||
dm := h.b.DERPMap()
|
||||
if dm == nil {
|
||||
st.Errors = append(st.Errors, "no DERP map (not connected?)")
|
||||
return
|
||||
}
|
||||
regStr := r.FormValue("region")
|
||||
var reg *tailcfg.DERPRegion
|
||||
if id, err := strconv.Atoi(regStr); err == nil {
|
||||
reg = dm.Regions[id]
|
||||
} else {
|
||||
for _, r := range dm.Regions {
|
||||
if r.RegionCode == regStr {
|
||||
reg = r
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if reg == nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("no such region %q in DERP map", regStr))
|
||||
return
|
||||
}
|
||||
st.Info = append(st.Info, fmt.Sprintf("Region %v == %q", reg.RegionID, reg.RegionCode))
|
||||
if len(dm.Regions) == 1 {
|
||||
st.Warnings = append(st.Warnings, "Having only a single DERP region (i.e. removing the default Tailscale-provided regions) is a single point of failure and could hamper connectivity")
|
||||
}
|
||||
|
||||
if reg.Avoid {
|
||||
st.Warnings = append(st.Warnings, "Region is marked with Avoid bit")
|
||||
}
|
||||
if len(reg.Nodes) == 0 {
|
||||
st.Errors = append(st.Errors, "Region has no nodes defined")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
var (
|
||||
dialer net.Dialer
|
||||
client *http.Client = http.DefaultClient
|
||||
)
|
||||
checkConn := func(derpNode *tailcfg.DERPNode) bool {
|
||||
port := firstNonzero(derpNode.DERPPort, 443)
|
||||
|
||||
var (
|
||||
hasIPv4 bool
|
||||
hasIPv6 bool
|
||||
)
|
||||
|
||||
// Check IPv4 first
|
||||
addr := net.JoinHostPort(firstNonzero(derpNode.IPv4, derpNode.HostName), strconv.Itoa(port))
|
||||
conn, err := dialer.DialContext(ctx, "tcp4", addr)
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ %q over IPv4: %v", derpNode.HostName, addr, err))
|
||||
} else {
|
||||
defer conn.Close()
|
||||
|
||||
// Upgrade to TLS and verify that works properly.
|
||||
tlsConn := tls.Client(conn, &tls.Config{
|
||||
ServerName: firstNonzero(derpNode.CertName, derpNode.HostName),
|
||||
})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error upgrading connection to node %q @ %q to TLS over IPv4: %v", derpNode.HostName, addr, err))
|
||||
} else {
|
||||
hasIPv4 = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check IPv6
|
||||
addr = net.JoinHostPort(firstNonzero(derpNode.IPv6, derpNode.HostName), strconv.Itoa(port))
|
||||
conn, err = dialer.DialContext(ctx, "tcp6", addr)
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ %q over IPv6: %v", derpNode.HostName, addr, err))
|
||||
} else {
|
||||
defer conn.Close()
|
||||
|
||||
// Upgrade to TLS and verify that works properly.
|
||||
tlsConn := tls.Client(conn, &tls.Config{
|
||||
ServerName: firstNonzero(derpNode.CertName, derpNode.HostName),
|
||||
// TODO(andrew-d): we should print more
|
||||
// detailed failure information on if/why TLS
|
||||
// verification fails
|
||||
})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error upgrading connection to node %q @ %q to TLS over IPv6: %v", derpNode.HostName, addr, err))
|
||||
} else {
|
||||
hasIPv6 = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we only have an IPv6 conn, then warn; we want both.
|
||||
if hasIPv6 && !hasIPv4 {
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Node %q only has IPv6 connectivity, not IPv4", derpNode.HostName))
|
||||
} else if hasIPv6 && hasIPv4 {
|
||||
st.Info = append(st.Info, fmt.Sprintf("Node %q has working IPv4 and IPv6 connectivity", derpNode.HostName))
|
||||
}
|
||||
|
||||
return hasIPv4 || hasIPv6
|
||||
}
|
||||
|
||||
checkSTUN4 := func(derpNode *tailcfg.DERPNode) {
|
||||
u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(h.logf, h.b.NetMon())).ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error creating IPv4 STUN listener: %v", err))
|
||||
return
|
||||
}
|
||||
defer u4.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var addr netip.Addr
|
||||
if derpNode.IPv4 != "" {
|
||||
addr, err = netip.ParseAddr(derpNode.IPv4)
|
||||
if err != nil {
|
||||
// Error printed elsewhere
|
||||
return
|
||||
}
|
||||
} else {
|
||||
addrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", derpNode.HostName)
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error resolving node %q IPv4 addresses: %v", derpNode.HostName, err))
|
||||
return
|
||||
}
|
||||
addr = addrs[0]
|
||||
}
|
||||
|
||||
addrPort := netip.AddrPortFrom(addr, uint16(firstNonzero(derpNode.STUNPort, 3478)))
|
||||
|
||||
txID := stun.NewTxID()
|
||||
req := stun.Request(txID)
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-done:
|
||||
}
|
||||
u4.Close()
|
||||
}()
|
||||
|
||||
gotResponse := make(chan netip.AddrPort, 1)
|
||||
go func() {
|
||||
defer u4.Close()
|
||||
|
||||
var buf [64 << 10]byte
|
||||
for {
|
||||
n, addr, err := u4.ReadFromUDPAddrPort(buf[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if !stun.Is(pkt) {
|
||||
continue
|
||||
}
|
||||
ap := netaddr.Unmap(addr)
|
||||
if !ap.IsValid() {
|
||||
continue
|
||||
}
|
||||
tx, addrPort, err := stun.ParseResponse(pkt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if tx == txID {
|
||||
gotResponse <- addrPort
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = u4.WriteToUDPAddrPort(req, addrPort)
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error sending IPv4 STUN packet to %v (%q): %v", addrPort, derpNode.HostName, err))
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-gotResponse:
|
||||
st.Info = append(st.Info, fmt.Sprintf("Node %q returned IPv4 STUN response: %v", derpNode.HostName, resp))
|
||||
case <-ctx.Done():
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Node %q did not return a IPv4 STUN response", derpNode.HostName))
|
||||
}
|
||||
}
|
||||
|
||||
// Start by checking whether we can establish a HTTP connection
|
||||
for _, derpNode := range reg.Nodes {
|
||||
connSuccess := checkConn(derpNode)
|
||||
|
||||
// Verify that the /generate_204 endpoint works
|
||||
captivePortalURL := "http://" + derpNode.HostName + "/generate_204"
|
||||
resp, err := client.Get(captivePortalURL)
|
||||
if err != nil {
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Error making request to the captive portal check %q; is port 80 blocked?", captivePortalURL))
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
if !connSuccess {
|
||||
continue
|
||||
}
|
||||
|
||||
fakePrivKey := key.NewNode()
|
||||
|
||||
// Next, repeatedly get the server key to see if the node is
|
||||
// behind a load balancer (incorrectly).
|
||||
serverPubKeys := make(map[key.NodePublic]bool)
|
||||
for i := range 5 {
|
||||
func() {
|
||||
rc := derphttp.NewRegionClient(fakePrivKey, h.logf, h.b.NetMon(), func() *tailcfg.DERPRegion {
|
||||
return &tailcfg.DERPRegion{
|
||||
RegionID: reg.RegionID,
|
||||
RegionCode: reg.RegionCode,
|
||||
RegionName: reg.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{derpNode},
|
||||
}
|
||||
})
|
||||
if err := rc.Connect(ctx); err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ try %d: %v", derpNode.HostName, i, err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(serverPubKeys) == 0 {
|
||||
st.Info = append(st.Info, fmt.Sprintf("Successfully established a DERP connection with node %q", derpNode.HostName))
|
||||
}
|
||||
serverPubKeys[rc.ServerPublicKey()] = true
|
||||
}()
|
||||
}
|
||||
if len(serverPubKeys) > 1 {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Received multiple server public keys (%d); is the DERP server behind a load balancer?", len(serverPubKeys)))
|
||||
}
|
||||
|
||||
// Send a STUN query to this node to verify whether or not it
|
||||
// correctly returns an IP address.
|
||||
checkSTUN4(derpNode)
|
||||
}
|
||||
|
||||
// TODO(bradfitz): finish:
|
||||
// * try to DERP auth with new public key.
|
||||
// * if rejected, add Info that it's likely the DERP server authz is on,
|
||||
// try with LocalBackend's node key instead.
|
||||
// * if they have more then one node, try to relay a packet between them
|
||||
// and see if it works (like cmd/derpprobe). But if server authz is on,
|
||||
// we won't be able to, so just warn. Say to turn that off, try again,
|
||||
// then turn it back on. TODO(bradfitz): maybe add a debug frame to DERP
|
||||
// protocol to say how many peers it's meshed with. Should match count
|
||||
// in DERPRegion. Or maybe even list all their server pub keys that it's peered
|
||||
// with.
|
||||
// * If their certificate is bad, either expired or just wrongly
|
||||
// issued in the first place, tell them specifically that the
|
||||
// cert is bad not just that the connection failed.
|
||||
}
|
||||
|
||||
func firstNonzero[T comparable](items ...T) T {
|
||||
var zero T
|
||||
for _, item := range items {
|
||||
if item != zero {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return zero
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,22 +7,3 @@
|
||||
// there's no CLI to get at the results anyway.
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
func init() {
|
||||
servePprofFunc = servePprof
|
||||
}
|
||||
|
||||
func servePprof(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.FormValue("name")
|
||||
switch name {
|
||||
case "profile":
|
||||
pprof.Profile(w, r)
|
||||
default:
|
||||
pprof.Handler(name).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
13
ipn/prefs.go
13
ipn/prefs.go
@@ -14,11 +14,9 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
@@ -28,7 +26,6 @@ import (
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
// DefaultControlURL is the URL base of the control plane
|
||||
@@ -241,10 +238,6 @@ type Prefs struct {
|
||||
// Linux-only.
|
||||
NetfilterKind string
|
||||
|
||||
// DriveShares are the configured DriveShares, stored in increasing order
|
||||
// by name.
|
||||
DriveShares []*drive.Share
|
||||
|
||||
// AllowSingleHosts was a legacy field that was always true
|
||||
// for the past 4.5 years. It controlled whether Tailscale
|
||||
// peers got /32 or /127 routes for each other.
|
||||
@@ -614,7 +607,6 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.AutoUpdate.Equals(p2.AutoUpdate) &&
|
||||
p.AppConnector == p2.AppConnector &&
|
||||
p.PostureChecking == p2.PostureChecking &&
|
||||
slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
|
||||
p.NetfilterKind == p2.NetfilterKind
|
||||
}
|
||||
|
||||
@@ -697,10 +689,7 @@ func (p PrefsView) ControlURLOrDefault() string {
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
// the default, then DefaultControlURL is returned instead.
|
||||
func (p *Prefs) ControlURLOrDefault() string {
|
||||
controlURL, err := syspolicy.GetString(syspolicy.ControlURL, p.ControlURL)
|
||||
if err != nil {
|
||||
controlURL = p.ControlURL
|
||||
}
|
||||
controlURL := p.ControlURL
|
||||
|
||||
if controlURL != "" {
|
||||
if controlURL != DefaultControlURL && IsLoginServerSynonym(controlURL) {
|
||||
|
||||
@@ -9,10 +9,8 @@ import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// New returns a new Store.
|
||||
@@ -53,19 +51,6 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromMap loads the in-memory cache from the provided map.
|
||||
// Any existing content is cleared, and the provided map is
|
||||
// copied into the cache.
|
||||
func (s *Store) LoadFromMap(m map[string][]byte) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
xmaps.Clear(s.cache)
|
||||
for k, v := range m {
|
||||
mak.Set(&s.cache, ipn.StateKey(k), v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadFromJSON attempts to unmarshal json content into the
|
||||
// in-memory cache.
|
||||
func (s *Store) LoadFromJSON(data []byte) error {
|
||||
|
||||
21
lanscaping-log.txt
Normal file
21
lanscaping-log.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
bradfitz@bradm4 tailscale.com % git pull origin main --rebase
|
||||
From github.com:tailscale/tailscale
|
||||
* branch main -> FETCH_HEAD
|
||||
Already up to date.
|
||||
bradfitz@bradm4 tailscale.com % go install ./cmd/tailscaled && ls -lh ~/go/bin/tailscaled
|
||||
-rwxr-xr-x@ 1 bradfitz staff 29M Jan 10 19:01 /Users/bradfitz/go/bin/tailscaled
|
||||
bradfitz@bradm4 tailscale.com % git rev-parse HEAD
|
||||
5fdb4f83ad23f0ee7a9dc08ecc2a0ceeabd81fc3bradfitz@bradm4 tailscale.com % go install -ldflags "-w -s" --tags=ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion ./cmd/tailscaled && ls -lh ~/go/bin/tailscaled
|
||||
-rwxr-xr-x@ 1 bradfitz staff 20M Jan 10 19:03 /Users/bradfitz/go/bin/tailscaled
|
||||
bradfitz@bradm4 tailscale.com % cat lanscaping-log.txt
|
||||
bradfitz@bradm4 tailscale.com % git pull origin main --rebase
|
||||
From github.com:tailscale/tailscale
|
||||
* branch main -> FETCH_HEAD
|
||||
Already up to date.
|
||||
bradfitz@bradm4 tailscale.com % go install ./cmd/tailscaled && ls -lh ~/go/bin/tailscaled
|
||||
-rwxr-xr-x@ 1 bradfitz staff 29M Jan 10 19:01 /Users/bradfitz/go/bin/tailscaledbradfitz@bradm4 tailscale.com % go build -o $HOME/bin/tailscaled.min -ldflags "-w -s" --tags=ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion ./cmd/tailscaled
|
||||
bradfitz@bradm4 tailscale.com % ls -lh $HOME/bin/tailscaled.min
|
||||
-rwxr-xr-x@ 1 bradfitz staff 20M Jan 10 19:05 /Users/bradfitz/bin/tailscaled.minbradfitz@bradm4 tailscale.com % make min
|
||||
./tool/go build -o OME/bin/tailscaled.min -ldflags "-w -s" --tags=ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_netstack ./cmd/tailscaled
|
||||
ls -lh OME/bin/tailscaled.min
|
||||
-rwxr-xr-x@ 1 bradfitz staff 16M Jan 10 20:03 OME/bin/tailscaled.min
|
||||
@@ -5,55 +5,12 @@ package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/illarion/gonotify/v2"
|
||||
"tailscale.com/health"
|
||||
)
|
||||
|
||||
func (m *directManager) runFileWatcher() {
|
||||
ctx, cancel := context.WithCancel(m.ctx)
|
||||
defer cancel()
|
||||
in, err := gonotify.NewInotify(ctx)
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is all best effort for now, to
|
||||
// surface warnings to users.
|
||||
m.logf("dns: inotify new: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
const events = gonotify.IN_ATTRIB |
|
||||
gonotify.IN_CLOSE_WRITE |
|
||||
gonotify.IN_CREATE |
|
||||
gonotify.IN_DELETE |
|
||||
gonotify.IN_MODIFY |
|
||||
gonotify.IN_MOVE
|
||||
|
||||
if err := in.AddWatch("/etc/", events); err != nil {
|
||||
m.logf("dns: inotify addwatch: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
events, err := in.Read()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
m.logf("dns: inotify read: %v", err)
|
||||
return
|
||||
}
|
||||
var match bool
|
||||
for _, ev := range events {
|
||||
if ev.Name == resolvConf {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
m.checkForFileTrample()
|
||||
}
|
||||
<-m.ctx.Done()
|
||||
}
|
||||
|
||||
var resolvTrampleWarnable = health.Register(&health.Warnable{
|
||||
|
||||
@@ -5,21 +5,17 @@ package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/cmpver"
|
||||
)
|
||||
|
||||
type kv struct {
|
||||
@@ -285,46 +281,10 @@ func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret
|
||||
}
|
||||
|
||||
func nmVersionBetween(first, last string) (bool, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return false, err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.Version")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
version, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected type %T for NM version", v.Value())
|
||||
}
|
||||
|
||||
outside := cmpver.Compare(version, first) < 0 || cmpver.Compare(version, last) > 0
|
||||
return !outside, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func nmIsUsingResolved() error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting NM mode: %w", err)
|
||||
}
|
||||
mode, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value())
|
||||
}
|
||||
if mode != "systemd-resolved" {
|
||||
return errors.New("NetworkManager is not using systemd-resolved for DNS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -390,42 +350,11 @@ func isLibnssResolveUsed(env newOSConfigEnv) error {
|
||||
}
|
||||
|
||||
func dbusPing(name, objectPath string) error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
obj := conn.Object(name, dbus.ObjectPath(objectPath))
|
||||
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
|
||||
return call.Err
|
||||
return errors.New("lanscaping")
|
||||
}
|
||||
|
||||
// dbusReadString reads a string property from the provided name and object
|
||||
// path. property must be in "interface.member" notation.
|
||||
func dbusReadString(name, objectPath, iface, member string) (string, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
obj := conn.Object(name, dbus.ObjectPath(objectPath))
|
||||
|
||||
var result dbus.Variant
|
||||
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, iface, member).Store(&result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if s, ok := result.Value().(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
return result.String(), nil
|
||||
return "", errors.New("lanscaping")
|
||||
}
|
||||
|
||||
354
net/dns/nm.go
354
net/dns/nm.go
@@ -6,17 +6,8 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/josharian/native"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,353 +26,22 @@ const reconfigTimeout = time.Second
|
||||
// nmManager uses the NetworkManager DBus API.
|
||||
type nmManager struct {
|
||||
interfaceName string
|
||||
manager dbus.BusObject
|
||||
dnsManager dbus.BusObject
|
||||
}
|
||||
|
||||
func newNMManager(interfaceName string) (*nmManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &nmManager{
|
||||
interfaceName: interfaceName,
|
||||
manager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager")),
|
||||
dnsManager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type nmConnectionSettings map[string]map[string]dbus.Variant
|
||||
|
||||
func (m *nmManager) SetDNS(config OSConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// NetworkManager only lets you set DNS settings on "active"
|
||||
// connections, which requires an assigned IP address. This got
|
||||
// configured before the DNS manager was invoked, but it might
|
||||
// take a little time for the netlink notifications to propagate
|
||||
// up. So, keep retrying for the duration of the reconfigTimeout.
|
||||
var err error
|
||||
for ctx.Err() == nil {
|
||||
err = m.trySet(ctx, config)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to system bus: %w", err)
|
||||
}
|
||||
|
||||
// This is how we get at the DNS settings:
|
||||
//
|
||||
// org.freedesktop.NetworkManager
|
||||
// |
|
||||
// [GetDeviceByIpIface]
|
||||
// |
|
||||
// v
|
||||
// org.freedesktop.NetworkManager.Device <--------\
|
||||
// (describes a network interface) |
|
||||
// | |
|
||||
// [GetAppliedConnection] [Reapply]
|
||||
// | |
|
||||
// v |
|
||||
// org.freedesktop.NetworkManager.Connection |
|
||||
// (connection settings) ------/
|
||||
// contains {dns, dns-priority, dns-search}
|
||||
//
|
||||
// Ref: https://developer.gnome.org/NetworkManager/stable/settings-ipv4.html.
|
||||
|
||||
nm := conn.Object(
|
||||
"org.freedesktop.NetworkManager",
|
||||
dbus.ObjectPath("/org/freedesktop/NetworkManager"),
|
||||
)
|
||||
|
||||
var devicePath dbus.ObjectPath
|
||||
err = nm.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
|
||||
m.interfaceName,
|
||||
).Store(&devicePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDeviceByIpIface: %w", err)
|
||||
}
|
||||
device := conn.Object("org.freedesktop.NetworkManager", devicePath)
|
||||
|
||||
var (
|
||||
settings nmConnectionSettings
|
||||
version uint64
|
||||
)
|
||||
err = device.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.Device.GetAppliedConnection", 0,
|
||||
uint32(0),
|
||||
).Store(&settings, &version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAppliedConnection: %w", err)
|
||||
}
|
||||
|
||||
// Frustratingly, NetworkManager represents IPv4 addresses as uint32s,
|
||||
// although IPv6 addresses are represented as byte arrays.
|
||||
// Perform the conversion here.
|
||||
var (
|
||||
dnsv4 []uint32
|
||||
dnsv6 [][]byte
|
||||
)
|
||||
for _, ip := range config.Nameservers {
|
||||
b := ip.As16()
|
||||
if ip.Is4() {
|
||||
dnsv4 = append(dnsv4, native.Endian.Uint32(b[12:]))
|
||||
} else {
|
||||
dnsv6 = append(dnsv6, b[:])
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkManager wipes out IPv6 address configuration unless we
|
||||
// tell it explicitly to keep it. Read out the current interface
|
||||
// settings and mirror them out to NetworkManager.
|
||||
var addrs6 []map[string]any
|
||||
if tsIf, err := net.InterfaceByName(m.interfaceName); err == nil {
|
||||
addrs, _ := tsIf.Addrs()
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netip.AddrFromSlice(ipnet.IP)
|
||||
nip = nip.Unmap()
|
||||
if ok && tsaddr.IsTailscaleIP(nip) && nip.Is6() {
|
||||
addrs6 = append(addrs6, map[string]any{
|
||||
"address": nip.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seen := map[dnsname.FQDN]bool{}
|
||||
var search []string
|
||||
for _, dom := range config.SearchDomains {
|
||||
if seen[dom] {
|
||||
continue
|
||||
}
|
||||
seen[dom] = true
|
||||
search = append(search, dom.WithTrailingDot())
|
||||
}
|
||||
for _, dom := range config.MatchDomains {
|
||||
if seen[dom] {
|
||||
continue
|
||||
}
|
||||
seen[dom] = true
|
||||
search = append(search, "~"+dom.WithTrailingDot())
|
||||
}
|
||||
if len(config.MatchDomains) == 0 {
|
||||
// Non-split routing requested, add an all-domains match.
|
||||
search = append(search, "~.")
|
||||
}
|
||||
|
||||
// Ideally we would like to disable LLMNR and mdns on the
|
||||
// interface here, but older NetworkManagers don't understand
|
||||
// those settings and choke on them, so we don't. Both LLMNR and
|
||||
// mdns will fail since tailscale0 doesn't do multicast, so it's
|
||||
// effectively fine. We used to try and enforce LLMNR and mdns
|
||||
// settings here, but that led to #1870.
|
||||
|
||||
ipv4Map := settings["ipv4"]
|
||||
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
|
||||
ipv4Map["dns-search"] = dbus.MakeVariant(search)
|
||||
// We should only request priority if we have nameservers to set.
|
||||
if len(dnsv4) == 0 {
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
} else if len(config.MatchDomains) > 0 {
|
||||
// Set a fairly high priority, but don't override all other
|
||||
// configs when in split-DNS mode.
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
|
||||
} else {
|
||||
// Negative priority means only the settings from the most
|
||||
// negative connection get used. The way this mixes with
|
||||
// per-domain routing is unclear, but it _seems_ that the
|
||||
// priority applies after routing has found possible
|
||||
// candidates for a resolution.
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(highestPriority)
|
||||
}
|
||||
|
||||
ipv6Map := settings["ipv6"]
|
||||
// In IPv6 settings, you're only allowed to provide additional
|
||||
// static DNS settings in "auto" (SLAAC) or "manual" mode. In
|
||||
// "manual" mode you also have to specify IP addresses, so we use
|
||||
// "auto".
|
||||
//
|
||||
// NM actually documents that to set just DNS servers, you should
|
||||
// use "auto" mode and then set ignore auto routes and DNS, which
|
||||
// basically means "autoconfigure but ignore any autoconfiguration
|
||||
// results you might get". As a safety, we also say that
|
||||
// NetworkManager should never try to make us the default route
|
||||
// (none of its business anyway, we handle our own default
|
||||
// routing).
|
||||
ipv6Map["method"] = dbus.MakeVariant("auto")
|
||||
if len(addrs6) > 0 {
|
||||
ipv6Map["address-data"] = dbus.MakeVariant(addrs6)
|
||||
}
|
||||
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
|
||||
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||
ipv6Map["never-default"] = dbus.MakeVariant(true)
|
||||
|
||||
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(search)
|
||||
if len(dnsv6) == 0 {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
} else if len(config.MatchDomains) > 0 {
|
||||
// Set a fairly high priority, but don't override all other
|
||||
// configs when in split-DNS mode.
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
|
||||
} else {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(highestPriority)
|
||||
}
|
||||
|
||||
// deprecatedProperties are the properties in interface settings
|
||||
// that are deprecated by NetworkManager.
|
||||
//
|
||||
// In practice, this means that they are returned for reading,
|
||||
// but submitting a settings object with them present fails
|
||||
// with hard-to-diagnose errors. They must be removed.
|
||||
deprecatedProperties := []string{
|
||||
"addresses", "routes",
|
||||
}
|
||||
|
||||
for _, property := range deprecatedProperties {
|
||||
delete(ipv4Map, property)
|
||||
delete(ipv6Map, property)
|
||||
}
|
||||
|
||||
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil {
|
||||
return fmt.Errorf("reapply: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, errors.New("lanscaping")
|
||||
}
|
||||
|
||||
func (m *nmManager) SupportsSplitDNS() bool {
|
||||
var mode string
|
||||
v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
mode, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Per NM's documentation, it only does split-DNS when it's
|
||||
// programming dnsmasq or systemd-resolved. All other modes are
|
||||
// primary-only.
|
||||
return mode == "dnsmasq" || mode == "systemd-resolved"
|
||||
func (m *nmManager) SetDNS(config OSConfig) error {
|
||||
return errors.New("lanscaping")
|
||||
}
|
||||
|
||||
func (m *nmManager) GetBaseConfig() (OSConfig, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration")
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
cfgs, ok := v.Value().([]map[string]dbus.Variant)
|
||||
if !ok {
|
||||
return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value())
|
||||
}
|
||||
|
||||
if len(cfgs) == 0 {
|
||||
return OSConfig{}, nil
|
||||
}
|
||||
|
||||
type dnsPrio struct {
|
||||
resolvers []netip.Addr
|
||||
domains []string
|
||||
priority int32
|
||||
}
|
||||
order := make([]dnsPrio, 0, len(cfgs)-1)
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
if name, ok := cfg["interface"]; ok {
|
||||
if s, ok := name.Value().(string); ok && s == m.interfaceName {
|
||||
// Config for the tailscale interface, skip.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var p dnsPrio
|
||||
|
||||
if v, ok := cfg["nameservers"]; ok {
|
||||
if ips, ok := v.Value().([]string); ok {
|
||||
for _, s := range ips {
|
||||
ip, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
// hmm, what do? Shouldn't really happen.
|
||||
continue
|
||||
}
|
||||
p.resolvers = append(p.resolvers, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := cfg["domains"]; ok {
|
||||
if domains, ok := v.Value().([]string); ok {
|
||||
p.domains = domains
|
||||
}
|
||||
}
|
||||
if v, ok := cfg["priority"]; ok {
|
||||
if prio, ok := v.Value().(int32); ok {
|
||||
p.priority = prio
|
||||
}
|
||||
}
|
||||
|
||||
order = append(order, p)
|
||||
}
|
||||
|
||||
sort.Slice(order, func(i, j int) bool {
|
||||
return order[i].priority < order[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
ret OSConfig
|
||||
seenResolvers = map[netip.Addr]bool{}
|
||||
seenSearch = map[string]bool{}
|
||||
)
|
||||
|
||||
for _, cfg := range order {
|
||||
for _, resolver := range cfg.resolvers {
|
||||
if seenResolvers[resolver] {
|
||||
continue
|
||||
}
|
||||
ret.Nameservers = append(ret.Nameservers, resolver)
|
||||
seenResolvers[resolver] = true
|
||||
}
|
||||
for _, dom := range cfg.domains {
|
||||
if seenSearch[dom] {
|
||||
continue
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret.SearchDomains = append(ret.SearchDomains, fqdn)
|
||||
seenSearch[dom] = true
|
||||
}
|
||||
if cfg.priority < 0 {
|
||||
// exclusive configurations preempt all other
|
||||
// configurations, so we're done.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return OSConfig{}, errors.New("lanscaping")
|
||||
}
|
||||
|
||||
func (m *nmManager) Close() error {
|
||||
|
||||
@@ -7,17 +7,11 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// DBus entities we talk to.
|
||||
@@ -34,12 +28,12 @@ import (
|
||||
// Clients connect to the bus and walk that same hierarchy to invoke
|
||||
// RPCs, get/set properties, or listen for signals.
|
||||
const (
|
||||
dbusResolvedObject = "org.freedesktop.resolve1"
|
||||
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
|
||||
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
||||
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
|
||||
dbusInterface = "org.freedesktop.DBus"
|
||||
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
||||
dbusResolvedObject = "org.freedesktop.resolve1"
|
||||
dbusResolvedPath = "/org/freedesktop/resolve1"
|
||||
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
||||
dbusPath = "/org/freedesktop/DBus"
|
||||
dbusInterface = "org.freedesktop.DBus"
|
||||
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
||||
)
|
||||
|
||||
type resolvedLinkNameserver struct {
|
||||
@@ -71,303 +65,11 @@ type resolvedManager struct {
|
||||
}
|
||||
|
||||
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) {
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
|
||||
mgr := &resolvedManager{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
logf: logf,
|
||||
health: health,
|
||||
ifidx: iface.Index,
|
||||
|
||||
configCR: make(chan changeRequest),
|
||||
}
|
||||
|
||||
go mgr.run(ctx)
|
||||
|
||||
return mgr, nil
|
||||
return nil, errors.New("lanscaping")
|
||||
}
|
||||
|
||||
func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
// NOTE: don't close this channel, since it's possible that the SetDNS
|
||||
// call will time out and return before the run loop answers, at which
|
||||
// point it will send on the now-closed channel.
|
||||
errc := make(chan error, 1)
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return m.ctx.Err()
|
||||
case m.configCR <- changeRequest{config, errc}:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return m.ctx.Err()
|
||||
case err := <-errc:
|
||||
if err != nil {
|
||||
m.logf("failed to configure resolved: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *resolvedManager) run(ctx context.Context) {
|
||||
var (
|
||||
conn *dbus.Conn
|
||||
signals chan *dbus.Signal
|
||||
rManager dbus.BusObject // rManager is the Resolved DBus connection
|
||||
)
|
||||
bo := backoff.NewBackoff("resolved-dbus", m.logf, 30*time.Second)
|
||||
needsReconnect := make(chan bool, 1)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Reconnect the systemBus if disconnected.
|
||||
reconnect := func() error {
|
||||
var err error
|
||||
signals = make(chan *dbus.Signal, 16)
|
||||
conn, err = dbus.SystemBus()
|
||||
if err != nil {
|
||||
m.logf("dbus connection error: %v", err)
|
||||
} else {
|
||||
m.logf("[v1] dbus connected")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Backoff increases time between reconnect attempts.
|
||||
go func() {
|
||||
bo.BackOff(ctx, err)
|
||||
needsReconnect <- true
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
rManager = conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath))
|
||||
|
||||
// Only receive the DBus signals we need to resync our config on
|
||||
// resolved restart. Failure to set filters isn't a fatal error,
|
||||
// we'll just receive all broadcast signals and have to ignore
|
||||
// them on our end.
|
||||
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
|
||||
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
||||
}
|
||||
conn.Signal(signals)
|
||||
|
||||
// Reset backoff and set osConfigurationSetWarnable to healthy after a successful reconnect.
|
||||
bo.BackOff(ctx, nil)
|
||||
m.health.SetHealthy(osConfigurationSetWarnable)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create initial systemBus connection.
|
||||
reconnect()
|
||||
|
||||
lastConfig := OSConfig{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if rManager == nil {
|
||||
return
|
||||
}
|
||||
// RevertLink resets all per-interface settings on systemd-resolved to defaults.
|
||||
// When ctx goes away systemd-resolved auto reverts.
|
||||
// Keeping for potential use in future refactor.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
|
||||
m.logf("[v1] RevertLink: %v", call.Err)
|
||||
return
|
||||
}
|
||||
return
|
||||
case configCR := <-m.configCR:
|
||||
// Track and update sync with latest config change.
|
||||
lastConfig = configCR.config
|
||||
|
||||
if rManager == nil {
|
||||
configCR.res <- fmt.Errorf("resolved DBus does not have a connection")
|
||||
continue
|
||||
}
|
||||
err := m.setConfigOverDBus(ctx, rManager, configCR.config)
|
||||
configCR.res <- err
|
||||
case <-needsReconnect:
|
||||
if err := reconnect(); err != nil {
|
||||
m.logf("[v1] SystemBus reconnect error %T", err)
|
||||
}
|
||||
continue
|
||||
case signal, ok := <-signals:
|
||||
// If signal ends and is nil then program tries to reconnect.
|
||||
if !ok {
|
||||
if err := reconnect(); err != nil {
|
||||
m.logf("[v1] SystemBus reconnect error %T", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// In theory the signal was filtered by DBus, but if
|
||||
// AddMatchSignal in the constructor failed, we may be
|
||||
// getting other spam.
|
||||
if signal.Path != dbusPath || signal.Name != dbusInterface+"."+dbusOwnerSignal {
|
||||
continue
|
||||
}
|
||||
if lastConfig.IsZero() {
|
||||
continue
|
||||
}
|
||||
// signal.Body is a []any of 3 strings: bus name, previous owner, new owner.
|
||||
if len(signal.Body) != 3 {
|
||||
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
|
||||
}
|
||||
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
|
||||
continue
|
||||
}
|
||||
newOwner, ok := signal.Body[2].(string)
|
||||
if !ok {
|
||||
m.logf("[unexpected] DBus NameOwnerChanged.new_owner is a %T, not a string", signal.Body[2])
|
||||
}
|
||||
if newOwner == "" {
|
||||
// systemd-resolved left the bus, no current owner,
|
||||
// nothing to do.
|
||||
continue
|
||||
}
|
||||
// The resolved bus name has a new owner, meaning resolved
|
||||
// restarted. Reprogram current config.
|
||||
m.logf("systemd-resolved restarted, syncing DNS config")
|
||||
err := m.setConfigOverDBus(ctx, rManager, lastConfig)
|
||||
// Set health while holding the lock, because this will
|
||||
// graciously serialize the resync's health outcome with a
|
||||
// concurrent SetDNS call.
|
||||
|
||||
if err != nil {
|
||||
m.logf("failed to configure systemd-resolved: %v", err)
|
||||
m.health.SetUnhealthy(osConfigurationSetWarnable, health.Args{health.ArgError: err.Error()})
|
||||
} else {
|
||||
m.health.SetHealthy(osConfigurationSetWarnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setConfigOverDBus updates resolved DBus config and is only called from the run goroutine.
|
||||
func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.BusObject, config OSConfig) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
|
||||
for i, server := range config.Nameservers {
|
||||
ip := server.As16()
|
||||
if server.Is4() {
|
||||
linkNameservers[i] = resolvedLinkNameserver{
|
||||
Family: unix.AF_INET,
|
||||
Address: ip[12:],
|
||||
}
|
||||
} else {
|
||||
linkNameservers[i] = resolvedLinkNameserver{
|
||||
Family: unix.AF_INET6,
|
||||
Address: ip[:],
|
||||
}
|
||||
}
|
||||
}
|
||||
err := rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
|
||||
m.ifidx, linkNameservers,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDNS: %w", err)
|
||||
}
|
||||
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains))
|
||||
seenDomains := map[dnsname.FQDN]bool{}
|
||||
for _, domain := range config.SearchDomains {
|
||||
if seenDomains[domain] {
|
||||
continue
|
||||
}
|
||||
seenDomains[domain] = true
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
Domain: domain.WithTrailingDot(),
|
||||
RoutingOnly: false,
|
||||
})
|
||||
}
|
||||
for _, domain := range config.MatchDomains {
|
||||
if seenDomains[domain] {
|
||||
// Search domains act as both search and match in
|
||||
// resolved, so it's correct to skip.
|
||||
continue
|
||||
}
|
||||
seenDomains[domain] = true
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
Domain: domain.WithTrailingDot(),
|
||||
RoutingOnly: true,
|
||||
})
|
||||
}
|
||||
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
|
||||
// Caller requested full DNS interception, install a
|
||||
// routing-only root domain.
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
Domain: ".",
|
||||
RoutingOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
err = rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
||||
m.ifidx, linkDomains,
|
||||
).Store()
|
||||
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
|
||||
// Issue 3188: older systemd-resolved had argument length limits.
|
||||
// Trim out the *.arpa. entries and try again.
|
||||
err = rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
||||
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
|
||||
).Store()
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
|
||||
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
|
||||
// but otherwise it's working good
|
||||
m.logf("[v1] failed to set SetLinkDefaultRoute: %v", call.Err)
|
||||
} else {
|
||||
return fmt.Errorf("setLinkDefaultRoute: %w", call.Err)
|
||||
}
|
||||
}
|
||||
|
||||
// Some best-effort setting of things, but resolved should do the
|
||||
// right thing if these fail (e.g. a really old resolved version
|
||||
// or something).
|
||||
|
||||
// Disable LLMNR, we don't do multicast.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
||||
}
|
||||
|
||||
// Disable mdns.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
||||
}
|
||||
|
||||
// We don't support dnssec consistently right now, force it off to
|
||||
// avoid partial failures when we split DNS internally.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
|
||||
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
||||
}
|
||||
return nil
|
||||
return errors.New("lanscaping")
|
||||
}
|
||||
|
||||
func (m *resolvedManager) SupportsSplitDNS() bool {
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/recursive"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
@@ -95,10 +94,8 @@ func (fr *fallbackResolver) Lookup(ctx context.Context, host string) ([]netip.Ad
|
||||
done = make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
fr.compareWithRecursive(ctx, addrsCh, host)
|
||||
}()
|
||||
} else {
|
||||
go fr.compareWithRecursive(ctx, addrsCh, host)
|
||||
}
|
||||
|
||||
addrs, err := lookup(ctx, host, fr.logf, fr.healthTracker, fr.netMon)
|
||||
@@ -117,98 +114,6 @@ func (fr *fallbackResolver) Lookup(ctx context.Context, host string) ([]netip.Ad
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// compareWithRecursive is responsible for comparing the DNS resolution
|
||||
// performed via the "normal" path (bootstrap DNS requests to the DERP servers)
|
||||
// with DNS resolution performed with our in-process recursive DNS resolver.
|
||||
//
|
||||
// It will select on addrsCh to read exactly one set of addrs (returned by the
|
||||
// "normal" path) and compare against the results returned by the recursive
|
||||
// resolver. If ctx is canceled, then it will abort.
|
||||
func (fr *fallbackResolver) compareWithRecursive(
|
||||
ctx context.Context,
|
||||
addrsCh <-chan []netip.Addr,
|
||||
host string,
|
||||
) {
|
||||
logf := logger.WithPrefix(fr.logf, "recursive: ")
|
||||
|
||||
// Ensure that we catch panics while we're testing this
|
||||
// code path; this should never panic, but we don't
|
||||
// want to take down the process by having the panic
|
||||
// propagate to the top of the goroutine's stack and
|
||||
// then terminate.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logf("bootstrap DNS: recovered panic: %v", r)
|
||||
metricRecursiveErrors.Add(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Don't resolve the same host multiple times
|
||||
// concurrently; if we end up in a tight loop, this can
|
||||
// take up a lot of CPU.
|
||||
var didRun bool
|
||||
result, err, _ := fr.sf.Do(host, func() (resolveResult, error) {
|
||||
didRun = true
|
||||
resolver := &recursive.Resolver{
|
||||
Dialer: netns.NewDialer(logf, fr.netMon),
|
||||
Logf: logf,
|
||||
}
|
||||
addrs, minTTL, err := resolver.Resolve(ctx, host)
|
||||
if err != nil {
|
||||
logf("error using recursive resolver: %v", err)
|
||||
metricRecursiveErrors.Add(1)
|
||||
return resolveResult{}, err
|
||||
}
|
||||
return resolveResult{addrs, minTTL}, nil
|
||||
})
|
||||
|
||||
// The singleflight function handled errors; return if
|
||||
// there was one. Additionally, don't bother doing the
|
||||
// comparison if we waited on another singleflight
|
||||
// caller; the results are likely to be the same, so
|
||||
// rather than spam the logs we can just exit and let
|
||||
// the singleflight call that did execute do the
|
||||
// comparison.
|
||||
//
|
||||
// Returning here is safe because the addrsCh channel
|
||||
// is buffered, so the main function won't block even
|
||||
// if we never read from it.
|
||||
if err != nil || !didRun {
|
||||
return
|
||||
}
|
||||
|
||||
addrs, minTTL := result.addrs, result.minTTL
|
||||
compareAddr := func(a, b netip.Addr) int { return a.Compare(b) }
|
||||
slices.SortFunc(addrs, compareAddr)
|
||||
|
||||
// Wait for a response from the main function; try this once before we
|
||||
// check whether the context is canceled since selects are
|
||||
// nondeterministic.
|
||||
var oldAddrs []netip.Addr
|
||||
select {
|
||||
case oldAddrs = <-addrsCh:
|
||||
// All good; continue
|
||||
default:
|
||||
// Now block.
|
||||
select {
|
||||
case oldAddrs = <-addrsCh:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
slices.SortFunc(oldAddrs, compareAddr)
|
||||
|
||||
matches := slices.Equal(addrs, oldAddrs)
|
||||
|
||||
logf("bootstrap DNS comparison: matches=%v oldAddrs=%v addrs=%v minTTL=%v", matches, oldAddrs, addrs, minTTL)
|
||||
|
||||
if matches {
|
||||
metricRecursiveMatches.Add(1)
|
||||
} else {
|
||||
metricRecursiveMismatches.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
func lookup(ctx context.Context, host string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) ([]netip.Addr, error) {
|
||||
if ip, err := netip.ParseAddr(host); err == nil && ip.IsValid() {
|
||||
return []netip.Addr{ip}, nil
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"net"
|
||||
@@ -20,19 +18,12 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/captivedetection"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/ping"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -222,19 +213,6 @@ type Client struct {
|
||||
// in tests to avoid probing the local LAN's router, etc.
|
||||
SkipExternalNetwork bool
|
||||
|
||||
// PortMapper, if non-nil, is used for portmap queries.
|
||||
// If nil, portmap discovery is not done.
|
||||
PortMapper *portmapper.Client // lazily initialized on first use
|
||||
|
||||
// UseDNSCache controls whether this client should use a
|
||||
// *dnscache.Resolver to resolve DERP hostnames, when no IP address is
|
||||
// provided in the DERP map. Note that Tailscale-provided DERP servers
|
||||
// all specify explicit IPv4 and IPv6 addresses, so this is mostly
|
||||
// helpful for users with custom DERP servers.
|
||||
//
|
||||
// If false, the default net.Resolver will be used, with no caching.
|
||||
UseDNSCache bool
|
||||
|
||||
// if non-zero, force this DERP region to be preferred in all reports where
|
||||
// the DERP is found to be reachable.
|
||||
ForcePreferredDERP int
|
||||
@@ -249,7 +227,6 @@ type Client struct {
|
||||
last *Report // most recent report
|
||||
lastFull time.Time // time of last full (non-incremental) report
|
||||
curState *reportState // non-nil if we're in a call to GetReport
|
||||
resolver *dnscache.Resolver // only set if UseDNSCache is true
|
||||
}
|
||||
|
||||
func (c *Client) enoughRegions() int {
|
||||
@@ -727,29 +704,6 @@ func (rs *reportState) setOptBool(b *opt.Bool, v bool) {
|
||||
b.Set(v)
|
||||
}
|
||||
|
||||
func (rs *reportState) probePortMapServices() {
|
||||
defer rs.waitPortMap.Done()
|
||||
|
||||
rs.setOptBool(&rs.report.UPnP, false)
|
||||
rs.setOptBool(&rs.report.PMP, false)
|
||||
rs.setOptBool(&rs.report.PCP, false)
|
||||
|
||||
res, err := rs.c.PortMapper.Probe(context.Background())
|
||||
if err != nil {
|
||||
if !errors.Is(err, portmapper.ErrGatewayRange) {
|
||||
// "skipping portmap; gateway range likely lacks support"
|
||||
// is not very useful, and too spammy on cloud systems.
|
||||
// If there are other errors, we want to log those.
|
||||
rs.c.logf("probePortMapServices: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rs.setOptBool(&rs.report.UPnP, res.UPnP)
|
||||
rs.setOptBool(&rs.report.PMP, res.PMP)
|
||||
rs.setOptBool(&rs.report.PCP, res.PCP)
|
||||
}
|
||||
|
||||
func newReport() *Report {
|
||||
return &Report{
|
||||
RegionLatency: make(map[int]time.Duration),
|
||||
@@ -809,8 +763,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
|
||||
ctx, cancel := context.WithTimeout(ctx, ReportTimeout)
|
||||
defer cancel()
|
||||
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelNetcheckClient, c.logf)
|
||||
|
||||
if dm == nil {
|
||||
return nil, errors.New("netcheck: GetReport: DERP map is nil")
|
||||
}
|
||||
@@ -888,11 +840,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
|
||||
v6udp.Close()
|
||||
}
|
||||
|
||||
if !c.SkipExternalNetwork && c.PortMapper != nil {
|
||||
rs.waitPortMap.Add(1)
|
||||
go rs.probePortMapServices()
|
||||
}
|
||||
|
||||
var plan probePlan
|
||||
if opts == nil || !opts.OnlyTCP443 {
|
||||
plan = makeProbePlan(dm, ifState, last, preferredDERP)
|
||||
@@ -913,9 +860,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
|
||||
|
||||
tmr := time.AfterFunc(c.captivePortalDelay(), func() {
|
||||
defer close(ch)
|
||||
d := captivedetection.NewDetector(c.logf)
|
||||
found := d.Detect(ctx, c.NetMon, dm, preferredDERP)
|
||||
rs.report.CaptivePortal.Set(found)
|
||||
})
|
||||
|
||||
captivePortalStop = func() {
|
||||
@@ -970,71 +914,8 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
|
||||
captivePortalStop()
|
||||
}
|
||||
|
||||
if !c.SkipExternalNetwork && c.PortMapper != nil {
|
||||
rs.waitPortMap.Wait()
|
||||
c.vlogf("portMap done")
|
||||
}
|
||||
rs.stopTimers()
|
||||
|
||||
// Try HTTPS and ICMP latency check if all STUN probes failed due to
|
||||
// UDP presumably being blocked.
|
||||
// TODO: this should be moved into the probePlan, using probeProto probeHTTPS.
|
||||
if !rs.anyUDP() && ctx.Err() == nil {
|
||||
var wg sync.WaitGroup
|
||||
var need []*tailcfg.DERPRegion
|
||||
for rid, reg := range dm.Regions {
|
||||
if !rs.haveRegionLatency(rid) && regionHasDERPNode(reg) {
|
||||
need = append(need, reg)
|
||||
}
|
||||
}
|
||||
if len(need) > 0 {
|
||||
if opts == nil || !opts.OnlyTCP443 {
|
||||
// Kick off ICMP in parallel to HTTPS checks; we don't
|
||||
// reuse the same WaitGroup for those probes because we
|
||||
// need to close the underlying Pinger after a timeout
|
||||
// or when all ICMP probes are done, regardless of
|
||||
// whether the HTTPS probes have finished.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := c.measureAllICMPLatency(ctx, rs, need); err != nil {
|
||||
c.logf("[v1] measureAllICMPLatency: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Add(len(need))
|
||||
c.logf("netcheck: UDP is blocked, trying HTTPS")
|
||||
}
|
||||
for _, reg := range need {
|
||||
go func(reg *tailcfg.DERPRegion) {
|
||||
defer wg.Done()
|
||||
if d, ip, err := c.measureHTTPSLatency(ctx, reg); err != nil {
|
||||
c.logf("[v1] netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
|
||||
} else {
|
||||
rs.mu.Lock()
|
||||
if l, ok := rs.report.RegionLatency[reg.RegionID]; !ok {
|
||||
mak.Set(&rs.report.RegionLatency, reg.RegionID, d)
|
||||
} else if l >= d {
|
||||
rs.report.RegionLatency[reg.RegionID] = d
|
||||
}
|
||||
// We set these IPv4 and IPv6 but they're not really used
|
||||
// and we don't necessarily set them both. If UDP is blocked
|
||||
// and both IPv4 and IPv6 are available over TCP, it's basically
|
||||
// random which fields end up getting set here.
|
||||
// Since they're not needed, that's fine for now.
|
||||
if ip.Is4() {
|
||||
rs.report.IPv4 = true
|
||||
}
|
||||
if ip.Is6() {
|
||||
rs.report.IPv6 = true
|
||||
}
|
||||
rs.mu.Unlock()
|
||||
}
|
||||
}(reg)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Wait for captive portal check before finishing the report.
|
||||
<-captivePortalDone
|
||||
|
||||
@@ -1109,165 +990,6 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
|
||||
return nil
|
||||
}
|
||||
|
||||
// measureHTTPSLatency measures HTTP request latency to the DERP region, but
|
||||
// only returns success if an HTTPS request to the region succeeds.
|
||||
func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netip.Addr, error) {
|
||||
metricHTTPSend.Add(1)
|
||||
ctx, cancel := context.WithTimeout(ctx, httpsProbeTimeout)
|
||||
defer cancel()
|
||||
|
||||
var ip netip.Addr
|
||||
|
||||
dc := derphttp.NewNetcheckClient(c.logf, c.NetMon)
|
||||
defer dc.Close()
|
||||
|
||||
// DialRegionTLS may dial multiple times if a node is not available, as such
|
||||
// it does not have stable timing to measure.
|
||||
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
}
|
||||
defer tcpConn.Close()
|
||||
|
||||
if ta, ok := tlsConn.RemoteAddr().(*net.TCPAddr); ok {
|
||||
ip, _ = netip.AddrFromSlice(ta.IP)
|
||||
ip = ip.Unmap()
|
||||
}
|
||||
if ip == (netip.Addr{}) {
|
||||
return 0, ip, fmt.Errorf("no unexpected RemoteAddr %#v", tlsConn.RemoteAddr())
|
||||
}
|
||||
|
||||
connc := make(chan *tls.Conn, 1)
|
||||
connc <- tlsConn
|
||||
|
||||
// make an HTTP request to measure, as this enables us to account for MITM
|
||||
// overhead in e.g. corp environments that have HTTP MITM in front of DERP.
|
||||
tr := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return nil, errors.New("unexpected DialContext dial")
|
||||
},
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
select {
|
||||
case nc := <-connc:
|
||||
return nc, nil
|
||||
default:
|
||||
return nil, errors.New("only one conn expected")
|
||||
}
|
||||
},
|
||||
}
|
||||
hc := &http.Client{Transport: tr}
|
||||
|
||||
// This is the request that will be measured, the request and response
|
||||
// should be small enough to fit into a single packet each way unless the
|
||||
// connection has already become unstable.
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+node.HostName+"/derp/latency-check", nil)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
}
|
||||
|
||||
startTime := c.timeNow()
|
||||
resp, err := hc.Do(req)
|
||||
reqDur := c.timeNow().Sub(startTime)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// DERPs should give us a nominal status code, so anything else is probably
|
||||
// an access denied by a MITM proxy (or at the very least a signal not to
|
||||
// trust this latency check).
|
||||
if resp.StatusCode > 299 {
|
||||
return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
_, err = io.Copy(io.Discard, io.LimitReader(resp.Body, 8<<10))
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
}
|
||||
|
||||
// return the connection duration, not the request duration, as this is the
|
||||
// best approximation of the RTT latency to the node. Note that the
|
||||
// connection setup performs happy-eyeballs and TLS so there are additional
|
||||
// overheads.
|
||||
return reqDur, ip, nil
|
||||
}
|
||||
|
||||
func (c *Client) measureAllICMPLatency(ctx context.Context, rs *reportState, need []*tailcfg.DERPRegion) error {
|
||||
if len(need) == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx, done := context.WithTimeout(ctx, icmpProbeTimeout)
|
||||
defer done()
|
||||
|
||||
p := ping.New(ctx, c.logf, netns.Listener(c.logf, c.NetMon))
|
||||
defer p.Close()
|
||||
|
||||
c.logf("UDP is blocked, trying ICMP")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(need))
|
||||
for _, reg := range need {
|
||||
go func(reg *tailcfg.DERPRegion) {
|
||||
defer wg.Done()
|
||||
if d, ok, err := c.measureICMPLatency(ctx, reg, p); err != nil {
|
||||
c.logf("[v1] measuring ICMP latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
|
||||
} else if ok {
|
||||
c.logf("[v1] ICMP latency of %v (%d): %v", reg.RegionCode, reg.RegionID, d)
|
||||
rs.mu.Lock()
|
||||
if l, ok := rs.report.RegionLatency[reg.RegionID]; !ok {
|
||||
mak.Set(&rs.report.RegionLatency, reg.RegionID, d)
|
||||
} else if l >= d {
|
||||
rs.report.RegionLatency[reg.RegionID] = d
|
||||
}
|
||||
|
||||
// We only send IPv4 ICMP right now
|
||||
rs.report.IPv4 = true
|
||||
rs.report.ICMPv4 = true
|
||||
|
||||
rs.mu.Unlock()
|
||||
}
|
||||
}(reg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) measureICMPLatency(ctx context.Context, reg *tailcfg.DERPRegion, p *ping.Pinger) (_ time.Duration, ok bool, err error) {
|
||||
if len(reg.Nodes) == 0 {
|
||||
return 0, false, fmt.Errorf("no nodes for region %d (%v)", reg.RegionID, reg.RegionCode)
|
||||
}
|
||||
|
||||
// Try pinging the first node in the region
|
||||
node := reg.Nodes[0]
|
||||
|
||||
if node.STUNPort < 0 {
|
||||
// If STUN is disabled on a node, interpret that as meaning don't measure latency.
|
||||
return 0, false, nil
|
||||
}
|
||||
const unusedPort = 0
|
||||
stunAddrPort, ok := c.nodeAddrPort(ctx, node, unusedPort, probeIPv4)
|
||||
if !ok {
|
||||
return 0, false, fmt.Errorf("no address for node %v (v4-for-icmp)", node.Name)
|
||||
}
|
||||
ip := stunAddrPort.Addr()
|
||||
addr := &net.IPAddr{
|
||||
IP: net.IP(ip.AsSlice()),
|
||||
Zone: ip.Zone(),
|
||||
}
|
||||
|
||||
// Use the unique node.Name field as the packet data to reduce the
|
||||
// likelihood that we get a mismatched echo response.
|
||||
d, err := p.Send(ctx, addr, []byte(node.Name))
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EPERM) {
|
||||
return 0, false, nil
|
||||
}
|
||||
return 0, false, err
|
||||
}
|
||||
return d, true, nil
|
||||
}
|
||||
|
||||
func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
|
||||
c.logf("[v1] report: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
fmt.Fprintf(w, "udp=%v", r.UDP)
|
||||
@@ -1636,23 +1358,6 @@ func (c *Client) nodeAddrPort(ctx context.Context, n *tailcfg.DERPNode, port int
|
||||
return naddrs, nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.UseDNSCache {
|
||||
if c.resolver == nil {
|
||||
c.resolver = &dnscache.Resolver{
|
||||
Forward: net.DefaultResolver,
|
||||
UseLastGood: true,
|
||||
Logf: c.logf,
|
||||
}
|
||||
}
|
||||
resolver := c.resolver
|
||||
lookupIPAddr = func(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
_, _, allIPs, err := resolver.LookupIP(ctx, host)
|
||||
return allIPs, err
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
probeIsV4 := proto == probeIPv4
|
||||
addrs, err := lookupIPAddr(ctx, n.HostName)
|
||||
for _, a := range addrs {
|
||||
|
||||
@@ -9,17 +9,13 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
"github.com/mdlayher/netlink"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
@@ -126,64 +122,8 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
v, err := defaultRouteInterfaceProcNet()
|
||||
if err == nil {
|
||||
d.InterfaceName = v
|
||||
return d, nil
|
||||
}
|
||||
// Issue 4038: the default route (such as on Unifi UDM Pro)
|
||||
// might be in a non-default table, so it won't show up in
|
||||
// /proc/net/route. Use netlink to find the default route.
|
||||
//
|
||||
// TODO(bradfitz): this allocates a fair bit. We should track
|
||||
// this in net/interfaces/monitor instead and have
|
||||
// interfaces.GetState take a netmon.Monitor or similar so the
|
||||
// routing table can be cached and the monitor's existing
|
||||
// subscription to route changes can update the cached state,
|
||||
// rather than querying the whole thing every time like
|
||||
// defaultRouteFromNetlink does.
|
||||
//
|
||||
// Then we should just always try to use the cached route
|
||||
// table from netlink every time, and only use /proc/net/route
|
||||
// as a fallback for weird environments where netlink might be
|
||||
// banned but /proc/net/route is emulated (e.g. stuff like
|
||||
// Cloud Run?).
|
||||
return defaultRouteFromNetlink()
|
||||
}
|
||||
|
||||
func defaultRouteFromNetlink() (d DefaultRouteDetails, err error) {
|
||||
c, err := rtnetlink.Dial(&netlink.Config{Strict: true})
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("defaultRouteFromNetlink: Dial: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
rms, err := c.Route.List()
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("defaultRouteFromNetlink: List: %w", err)
|
||||
}
|
||||
for _, rm := range rms {
|
||||
if rm.Attributes.Gateway == nil {
|
||||
// A default route has a gateway. If it doesn't, skip it.
|
||||
continue
|
||||
}
|
||||
if rm.Attributes.Dst != nil {
|
||||
// A default route has a nil destination to mean anything
|
||||
// so ignore any route for a specific destination.
|
||||
// TODO(bradfitz): better heuristic?
|
||||
// empirically this seems like enough.
|
||||
continue
|
||||
}
|
||||
// TODO(bradfitz): care about address family, if
|
||||
// callers ever start caring about v4-vs-v6 default
|
||||
// route differences.
|
||||
idx := int(rm.Attributes.OutIface)
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
if iface, err := net.InterfaceByIndex(idx); err == nil {
|
||||
d.InterfaceName = iface.Name
|
||||
d.InterfaceIndex = idx
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
return d, errNoDefaultRoute
|
||||
return d, err
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
|
||||
@@ -128,7 +128,7 @@ func New(logf logger.Logf) (*Monitor, error) {
|
||||
}
|
||||
m.ifState = st
|
||||
|
||||
m.om, err = newOSMon(logf, m)
|
||||
m.om, err = newPollingMon(logf, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build lanscaping_always_false
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
@@ -17,13 +19,6 @@ import (
|
||||
|
||||
const debugRouteMessages = false
|
||||
|
||||
// unspecifiedMessage is a minimal message implementation that should not
|
||||
// be ignored. In general, OS-specific implementations should use better
|
||||
// types and avoid this if they can.
|
||||
type unspecifiedMessage struct{}
|
||||
|
||||
func (unspecifiedMessage) ignore() bool { return false }
|
||||
|
||||
func newOSMon(logf logger.Logf, _ *Monitor) (osMon, error) {
|
||||
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !android
|
||||
//go:build !android && lanscaping_always_false
|
||||
|
||||
package netmon
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (!linux && !freebsd && !windows && !darwin) || android
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package netmon
|
||||
|
||||
import (
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
// LoginEndpointForProxyDetermination is the URL used for testing
|
||||
@@ -485,19 +483,6 @@ func GetState() (*State, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if s.AnyInterfaceUp() {
|
||||
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
|
||||
s.HTTPProxy = u.String()
|
||||
}
|
||||
if getPAC != nil {
|
||||
s.PAC = getPAC()
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !js
|
||||
//go:build !ios && !js && lanscaping_always_false
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
@@ -19,27 +17,7 @@ import (
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func UpdateSrcAddr(q *packet.Parsed, src netip.Addr) {
|
||||
if src.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if src.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidSNAT = true
|
||||
q.CaptureMeta.OriginalSrc = q.Src
|
||||
|
||||
old := q.Src.Addr()
|
||||
q.Src = netip.AddrPortFrom(src, q.Src.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if src.Is6() {
|
||||
v6 := src.As16()
|
||||
copy(b[8:24], v6[:])
|
||||
updateV6PacketChecksums(q, old, src)
|
||||
} else {
|
||||
v4 := src.As4()
|
||||
copy(b[12:16], v4[:])
|
||||
updateV4PacketChecksums(q, old, src)
|
||||
}
|
||||
panic("lanscaping")
|
||||
}
|
||||
|
||||
// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
|
||||
@@ -47,29 +25,15 @@ func UpdateSrcAddr(q *packet.Parsed, src netip.Addr) {
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func UpdateDstAddr(q *packet.Parsed, dst netip.Addr) {
|
||||
if dst.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if dst.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidDNAT = true
|
||||
q.CaptureMeta.OriginalDst = q.Dst
|
||||
|
||||
old := q.Dst.Addr()
|
||||
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if dst.Is6() {
|
||||
v6 := dst.As16()
|
||||
copy(b[24:40], v6[:])
|
||||
updateV6PacketChecksums(q, old, dst)
|
||||
} else {
|
||||
v4 := dst.As4()
|
||||
copy(b[16:20], v4[:])
|
||||
updateV4PacketChecksums(q, old, dst)
|
||||
}
|
||||
panic("lanscaping")
|
||||
}
|
||||
|
||||
const (
|
||||
headerUDPMinimumSize = 8 // header.UDPMinimumSize
|
||||
headerTCPMinimumSize = 20 // header.TCPMinimumSize
|
||||
headerICMPv6MinimumSize = 8 // header.ICMPv6MinimumSize
|
||||
)
|
||||
|
||||
// updateV4PacketChecksums updates the checksums in the packet buffer.
|
||||
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
|
||||
// p is modified in place.
|
||||
@@ -88,13 +52,13 @@ func updateV4PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
if len(tr) < headerUDPMinimumSize {
|
||||
// Not enough space for a UDP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[6:8], o4[:], n4[:])
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
if len(tr) < headerTCPMinimumSize {
|
||||
// Not enough space for a TCP header.
|
||||
return
|
||||
}
|
||||
@@ -116,33 +80,7 @@ func updateV4PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, no checksums are updated.
|
||||
func updateV6PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 40 {
|
||||
// Not enough space for an IPv6 header.
|
||||
return
|
||||
}
|
||||
o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.ICMPv6:
|
||||
if len(tr) < header.ICMPv6MinimumSize {
|
||||
return
|
||||
}
|
||||
header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.SCTP:
|
||||
// No transport layer update required.
|
||||
}
|
||||
panic("lanscaping")
|
||||
}
|
||||
|
||||
// updateV4Checksum calculates and updates the checksum in the packet buffer for
|
||||
|
||||
@@ -218,10 +218,6 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
|
||||
return
|
||||
case ipproto.TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
case ipproto.Fragment:
|
||||
// An IPProto value of 0xff (our Fragment constant for internal use)
|
||||
// should never actually be used in the wild; if we see it,
|
||||
@@ -321,10 +317,6 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
|
||||
return
|
||||
case ipproto.TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
case ipproto.Fragment:
|
||||
// An IPProto value of 0xff (our Fragment constant for internal use)
|
||||
// should never actually be used in the wild; if we see it,
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
|
||||
// Tailscale-specific messages between nodes. It uses IP protocol 99
|
||||
// (reserved for "any private encryption scheme") within
|
||||
// WireGuard's normal encryption between peers and never hits the host
|
||||
// network stack.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/net/flowtrack"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
// TailscaleRejectedHeader is a TSMP message that says that one
|
||||
// Tailscale node has rejected the connection from another. Unlike a
|
||||
// TCP RST, this includes a reason.
|
||||
//
|
||||
// On the wire, after the IP header, it's currently 7 or 8 bytes:
|
||||
// - '!'
|
||||
// - IPProto byte (IANA protocol number: TCP or UDP)
|
||||
// - 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
|
||||
// - srcPort big endian uint16
|
||||
// - dstPort big endian uint16
|
||||
// - [optional] byte of flag bits:
|
||||
// lowest bit (0x1): MaybeBroken
|
||||
//
|
||||
// In the future it might also accept 16 byte IP flow src/dst IPs
|
||||
// after the header, if they're different than the IP-level ones.
|
||||
type TailscaleRejectedHeader struct {
|
||||
IPSrc netip.Addr // IPv4 or IPv6 header's src IP
|
||||
IPDst netip.Addr // IPv4 or IPv6 header's dst IP
|
||||
Src netip.AddrPort // rejected flow's src
|
||||
Dst netip.AddrPort // rejected flow's dst
|
||||
Proto ipproto.Proto // proto that was rejected (TCP or UDP)
|
||||
Reason TailscaleRejectReason // why the connection was rejected
|
||||
|
||||
// MaybeBroken is whether the rejection is non-terminal (the
|
||||
// client should not fail immediately). This is sent by a
|
||||
// target when it's not sure whether it's totally broken, but
|
||||
// it might be. For example, the target tailscaled might think
|
||||
// its host firewall or IP forwarding aren't configured
|
||||
// properly, but tailscaled might be wrong (not having enough
|
||||
// visibility into what the OS is doing). When true, the
|
||||
// message is simply an FYI as a potential reason to use for
|
||||
// later when the pendopen connection tracking timer expires.
|
||||
MaybeBroken bool
|
||||
}
|
||||
|
||||
const rejectFlagBitMaybeBroken = 0x1
|
||||
|
||||
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
|
||||
return flowtrack.MakeTuple(rh.Proto, rh.Src, rh.Dst)
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) String() string {
|
||||
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
|
||||
type TSMPType uint8
|
||||
|
||||
const (
|
||||
// TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader.
|
||||
TSMPTypeRejectedConn TSMPType = '!'
|
||||
|
||||
// TSMPTypePing is the type byte for a TailscalePingRequest.
|
||||
TSMPTypePing TSMPType = 'p'
|
||||
|
||||
// TSMPTypePong is the type byte for a TailscalePongResponse.
|
||||
TSMPTypePong TSMPType = 'o'
|
||||
)
|
||||
|
||||
type TailscaleRejectReason byte
|
||||
|
||||
// IsZero reports whether r is the zero value, representing no rejection.
|
||||
func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone }
|
||||
|
||||
const (
|
||||
// TailscaleRejectReasonNone is the TailscaleRejectReason zero value.
|
||||
TailscaleRejectReasonNone TailscaleRejectReason = 0
|
||||
|
||||
// RejectedDueToACLs means that the host rejected the connection due to ACLs.
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
|
||||
// RejectedDueToShieldsUp means that the host rejected the connection due to shields being up.
|
||||
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
|
||||
|
||||
// RejectedDueToIPForwarding means that the relay node's IP
|
||||
// forwarding is disabled.
|
||||
RejectedDueToIPForwarding TailscaleRejectReason = 'F'
|
||||
|
||||
// RejectedDueToHostFirewall means that the target host's
|
||||
// firewall is blocking the traffic.
|
||||
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
|
||||
)
|
||||
|
||||
func (r TailscaleRejectReason) String() string {
|
||||
switch r {
|
||||
case RejectedDueToACLs:
|
||||
return "acl"
|
||||
case RejectedDueToShieldsUp:
|
||||
return "shields"
|
||||
case RejectedDueToIPForwarding:
|
||||
return "host-ip-forwarding-unavailable"
|
||||
case RejectedDueToHostFirewall:
|
||||
return "host-firewall"
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", byte(r))
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) hasFlags() bool {
|
||||
return h.MaybeBroken // the only one currently
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Len() int {
|
||||
v := 1 + // TSMPType byte
|
||||
1 + // IPProto byte
|
||||
1 + // TailscaleRejectReason byte
|
||||
2*2 // 2 uint16 ports
|
||||
if h.IPSrc.Is4() {
|
||||
v += ip4HeaderLength
|
||||
} else if h.IPSrc.Is6() {
|
||||
v += ip6HeaderLength
|
||||
}
|
||||
if h.hasFlags() {
|
||||
v++
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if h.Src.Addr().Is4() {
|
||||
iph := IP4Header{
|
||||
IPProto: ipproto.TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip4HeaderLength:]
|
||||
} else if h.Src.Addr().Is6() {
|
||||
iph := IP6Header{
|
||||
IPProto: ipproto.TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip6HeaderLength:]
|
||||
} else {
|
||||
return errors.New("bogus src IP")
|
||||
}
|
||||
buf[0] = byte(TSMPTypeRejectedConn)
|
||||
buf[1] = byte(h.Proto)
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port())
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port())
|
||||
|
||||
if h.hasFlags() {
|
||||
var flags byte
|
||||
if h.MaybeBroken {
|
||||
flags |= rejectFlagBitMaybeBroken
|
||||
}
|
||||
buf[7] = flags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsTailscaleRejectedHeader parses pp as an incoming rejection
|
||||
// connection TSMP message.
|
||||
//
|
||||
// ok reports whether pp was a valid TSMP rejection packet.
|
||||
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
|
||||
p := pp.Payload()
|
||||
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
|
||||
return
|
||||
}
|
||||
h = TailscaleRejectedHeader{
|
||||
Proto: ipproto.Proto(p[1]),
|
||||
Reason: TailscaleRejectReason(p[2]),
|
||||
IPSrc: pp.Src.Addr(),
|
||||
IPDst: pp.Dst.Addr(),
|
||||
Src: netip.AddrPortFrom(pp.Dst.Addr(), binary.BigEndian.Uint16(p[3:5])),
|
||||
Dst: netip.AddrPortFrom(pp.Src.Addr(), binary.BigEndian.Uint16(p[5:7])),
|
||||
}
|
||||
if len(p) > 7 {
|
||||
flags := p[7]
|
||||
h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
|
||||
// TSMPPingRequest is a TSMP message that's like an ICMP ping request.
|
||||
//
|
||||
// On the wire, after the IP header, it's currently 9 bytes:
|
||||
// - 'p' (TSMPTypePing)
|
||||
// - 8 opaque ping bytes to copy back in the response
|
||||
type TSMPPingRequest struct {
|
||||
Data [8]byte
|
||||
}
|
||||
|
||||
func (pp *Parsed) AsTSMPPing() (h TSMPPingRequest, ok bool) {
|
||||
if pp.IPProto != ipproto.TSMP {
|
||||
return
|
||||
}
|
||||
p := pp.Payload()
|
||||
if len(p) < 9 || p[0] != byte(TSMPTypePing) {
|
||||
return
|
||||
}
|
||||
copy(h.Data[:], p[1:])
|
||||
return h, true
|
||||
}
|
||||
|
||||
type TSMPPongReply struct {
|
||||
IPHeader Header
|
||||
Data [8]byte
|
||||
PeerAPIPort uint16
|
||||
}
|
||||
|
||||
// AsTSMPPong returns pp as a TSMPPongReply and whether it is one.
|
||||
// The pong.IPHeader field is not populated.
|
||||
func (pp *Parsed) AsTSMPPong() (pong TSMPPongReply, ok bool) {
|
||||
if pp.IPProto != ipproto.TSMP {
|
||||
return
|
||||
}
|
||||
p := pp.Payload()
|
||||
if len(p) < 9 || p[0] != byte(TSMPTypePong) {
|
||||
return
|
||||
}
|
||||
copy(pong.Data[:], p[1:])
|
||||
if len(p) >= 11 {
|
||||
pong.PeerAPIPort = binary.BigEndian.Uint16(p[9:])
|
||||
}
|
||||
return pong, true
|
||||
}
|
||||
|
||||
func (h TSMPPongReply) Len() int {
|
||||
return h.IPHeader.Len() + 11
|
||||
}
|
||||
|
||||
func (h TSMPPongReply) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if err := h.IPHeader.Marshal(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[h.IPHeader.Len():]
|
||||
buf[0] = byte(TSMPTypePong)
|
||||
copy(buf[1:], h.Data[:])
|
||||
binary.BigEndian.PutUint16(buf[9:11], h.PeerAPIPort)
|
||||
return nil
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/tlsdial/blockblame"
|
||||
)
|
||||
|
||||
var counterFallbackOK int32 // atomic
|
||||
@@ -107,19 +106,6 @@ func Config(host string, ht *health.Tracker, base *tls.Config) *tls.Config {
|
||||
}
|
||||
if ht != nil {
|
||||
defer func() {
|
||||
if retErr != nil && cert != nil {
|
||||
// Is it a MITM SSL certificate from a well-known network appliance manufacturer?
|
||||
// Show a dedicated warning.
|
||||
m, ok := blockblame.VerifyCertificate(cert)
|
||||
if ok {
|
||||
log.Printf("tlsdial: server cert for %q looks like %q equipment (could be blocking Tailscale)", host, m.Name)
|
||||
ht.SetUnhealthy(mitmBlockWarnable, health.Args{"manufacturer": m.Name})
|
||||
} else {
|
||||
ht.SetHealthy(mitmBlockWarnable)
|
||||
}
|
||||
} else {
|
||||
ht.SetHealthy(mitmBlockWarnable)
|
||||
}
|
||||
if retErr != nil && selfSignedIssuer != "" {
|
||||
// Self-signed certs are never valid.
|
||||
//
|
||||
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// dnsMap maps MagicDNS names (both base + FQDN) to their first IP.
|
||||
@@ -28,55 +25,6 @@ func canonMapKey(s string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(s, "."))
|
||||
}
|
||||
|
||||
func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make(dnsMap)
|
||||
suffix := nm.MagicDNSSuffix()
|
||||
have4 := false
|
||||
addrs := nm.GetAddresses()
|
||||
if nm.Name != "" && addrs.Len() > 0 {
|
||||
ip := addrs.At(0).Addr()
|
||||
ret[canonMapKey(nm.Name)] = ip
|
||||
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(nm.Name, suffix))] = ip
|
||||
}
|
||||
for _, p := range addrs.All() {
|
||||
if p.Addr().Is4() {
|
||||
have4 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.Name() == "" {
|
||||
continue
|
||||
}
|
||||
for _, pfx := range p.Addresses().All() {
|
||||
ip := pfx.Addr()
|
||||
if ip.Is4() && !have4 {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(p.Name())] = ip
|
||||
if dnsname.HasSuffix(p.Name(), suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(p.Name(), suffix))] = ip
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
if rec.Type != "" {
|
||||
continue
|
||||
}
|
||||
ip, err := netip.ParseAddr(rec.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(rec.Name)] = ip
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// errUnresolved is a sentinel error returned by dnsMap.resolveMemory.
|
||||
var errUnresolved = errors.New("address well formed but not resolved")
|
||||
|
||||
|
||||
@@ -2,99 +2,3 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
)
|
||||
|
||||
// dohConn is a net.PacketConn suitable for returning from
|
||||
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
|
||||
// ExitDNS DoH proxy service.
|
||||
type dohConn struct {
|
||||
ctx context.Context
|
||||
baseURL string
|
||||
hc *http.Client // if nil, default is used
|
||||
dnsCache *dnscache.MessageCache
|
||||
|
||||
rbuf bytes.Buffer
|
||||
}
|
||||
|
||||
var (
|
||||
_ net.Conn = (*dohConn)(nil)
|
||||
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
|
||||
)
|
||||
|
||||
func (*dohConn) Close() error { return nil }
|
||||
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
|
||||
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
|
||||
func (*dohConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
return c.Write(p)
|
||||
}
|
||||
|
||||
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
n, err = c.Read(p)
|
||||
return n, todoAddr{}, err
|
||||
}
|
||||
|
||||
func (c *dohConn) Read(p []byte) (n int, err error) {
|
||||
return c.rbuf.Read(p)
|
||||
}
|
||||
|
||||
func (c *dohConn) Write(packet []byte) (n int, err error) {
|
||||
if c.dnsCache != nil {
|
||||
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
|
||||
if err == nil {
|
||||
// Cache hit.
|
||||
// TODO(bradfitz): add clientmetric
|
||||
return len(packet), nil
|
||||
}
|
||||
c.rbuf.Reset()
|
||||
}
|
||||
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
const dohType = "application/dns-message"
|
||||
req.Header.Set("Content-Type", dohType)
|
||||
hc := c.hc
|
||||
if hc == nil {
|
||||
hc = http.DefaultClient
|
||||
}
|
||||
hres, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer hres.Body.Close()
|
||||
if hres.StatusCode != 200 {
|
||||
return 0, errors.New(hres.Status)
|
||||
}
|
||||
if ct := hres.Header.Get("Content-Type"); ct != dohType {
|
||||
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
|
||||
}
|
||||
_, err = io.Copy(&c.rbuf, hres.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
|
||||
}
|
||||
return len(packet), nil
|
||||
}
|
||||
|
||||
type todoAddr struct{}
|
||||
|
||||
func (todoAddr) Network() string { return "unused" }
|
||||
func (todoAddr) String() string { return "unused-todoAddr" }
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -19,17 +18,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// NewDialer returns a new Dialer that can dial out of tailscaled.
|
||||
@@ -76,12 +72,10 @@ type Dialer struct {
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
netMon *netmon.Monitor
|
||||
netMonUnregister func()
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
dnsCache *dnscache.MessageCache // nil until first non-empty SetExitDNSDoH
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
nextSysConnID int
|
||||
activeSysConns map[int]net.Conn // active connections not yet closed
|
||||
}
|
||||
@@ -117,26 +111,6 @@ func (d *Dialer) TUNName() string {
|
||||
return d.tunName
|
||||
}
|
||||
|
||||
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
|
||||
// The doh URL should contain the scheme, authority, and path, but without
|
||||
// a '?' and/or query parameters.
|
||||
//
|
||||
// For example, "http://100.68.82.120:47830/dns-query".
|
||||
func (d *Dialer) SetExitDNSDoH(doh string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.exitDNSDoHBase == doh {
|
||||
return
|
||||
}
|
||||
d.exitDNSDoHBase = doh
|
||||
if doh != "" && d.dnsCache == nil {
|
||||
d.dnsCache = new(dnscache.MessageCache)
|
||||
}
|
||||
if d.dnsCache != nil {
|
||||
d.dnsCache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// SetRoutes configures the dialer to dial the specified routes via Tailscale,
|
||||
// and the specified localRoutes using the default interface.
|
||||
func (d *Dialer) SetRoutes(routes, localRoutes []netip.Prefix) {
|
||||
@@ -287,59 +261,9 @@ func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.R
|
||||
// SetNetMap sets the current network map and notably, the DNS names
|
||||
// in its DNS configuration.
|
||||
func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
|
||||
m := dnsMapFromNetworkMap(nm)
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.dns = m
|
||||
}
|
||||
|
||||
// userDialResolve resolves addr as if a user initiating the dial. (e.g. from a
|
||||
// SOCKS or HTTP outbound proxy)
|
||||
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netip.AddrPort, error) {
|
||||
d.mu.Lock()
|
||||
dns := d.dns
|
||||
exitDNSDoH := d.exitDNSDoHBase
|
||||
d.mu.Unlock()
|
||||
|
||||
// MagicDNS or otherwise baked into the NetworkMap? Try that first.
|
||||
ipp, err := dns.resolveMemory(ctx, network, addr)
|
||||
if err != errUnresolved {
|
||||
return ipp, err
|
||||
}
|
||||
|
||||
// Otherwise, hit the network.
|
||||
|
||||
// TODO(bradfitz): wire up net/dnscache too.
|
||||
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
// addr is malformed.
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
|
||||
var r net.Resolver
|
||||
if exitDNSDoH != "" && runtime.GOOS != "windows" { // Windows: https://github.com/golang/go/issues/33097
|
||||
r.PreferGo = true
|
||||
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return &dohConn{
|
||||
ctx: ctx,
|
||||
baseURL: exitDNSDoH,
|
||||
hc: d.PeerAPIHTTPClient(),
|
||||
dnsCache: d.dnsCache,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return netip.AddrPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
|
||||
}
|
||||
ip, _ := netip.AddrFromSlice(ips[0])
|
||||
return netip.AddrPortFrom(ip.Unmap(), port), nil
|
||||
}
|
||||
|
||||
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
|
||||
@@ -400,44 +324,6 @@ func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserDial connects to the provided network address as if a user were
|
||||
// initiating the dial. (e.g. from a SOCKS or HTTP outbound proxy)
|
||||
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := d.userDialResolve(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
|
||||
if d.NetstackDialTCP == nil || d.NetstackDialUDP == nil {
|
||||
return nil, errors.New("Dialer not initialized correctly")
|
||||
}
|
||||
if strings.HasPrefix(network, "udp") {
|
||||
return d.NetstackDialUDP(ctx, ipp)
|
||||
}
|
||||
return d.NetstackDialTCP(ctx, ipp)
|
||||
}
|
||||
|
||||
if routes := d.routes.Load(); routes != nil {
|
||||
if isTailscaleRoute, _ := routes.Lookup(ipp.Addr()); isTailscaleRoute {
|
||||
return d.getPeerDialer().DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
|
||||
return d.SystemDial(ctx, network, ipp.String())
|
||||
}
|
||||
|
||||
// Workaround for macOS for now: dial Tailscale IPs with peer dialer.
|
||||
// TODO(bradfitz): fix dialing subnet routers, public IPs via exit nodes,
|
||||
// etc. This is a temporary partial for macOS. We need to plumb ART tables &
|
||||
// prefs & host routing table updates around in more places. We just don't
|
||||
// know from the limited context here how to dial properly.
|
||||
if version.IsMacGUIVariant() && tsaddr.IsTailscaleIP(ipp.Addr()) {
|
||||
return d.getPeerDialer().DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
// TODO(bradfitz): netns, etc
|
||||
var stdDialer net.Dialer
|
||||
return stdDialer.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
|
||||
// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
|
||||
//
|
||||
// network must a "tcp" type, and addr must be an ip:port. Name resolution
|
||||
|
||||
@@ -2,62 +2,3 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tstun
|
||||
|
||||
import (
|
||||
"github.com/mdlayher/genetlink"
|
||||
"github.com/mdlayher/netlink"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// setLinkSpeed sets the advertised link speed of the TUN interface.
|
||||
func setLinkSpeed(iface tun.Device, mbps int) error {
|
||||
name, err := iface.Name()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := genetlink.Dial(&netlink.Config{Strict: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
f, err := conn.GetFamily(unix.ETHTOOL_GENL_NAME)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ae := netlink.NewAttributeEncoder()
|
||||
ae.Nested(unix.ETHTOOL_A_LINKMODES_HEADER, func(nae *netlink.AttributeEncoder) error {
|
||||
nae.String(unix.ETHTOOL_A_HEADER_DEV_NAME, name)
|
||||
return nil
|
||||
})
|
||||
ae.Uint32(unix.ETHTOOL_A_LINKMODES_SPEED, uint32(mbps))
|
||||
|
||||
b, err := ae.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Execute(
|
||||
genetlink.Message{
|
||||
Header: genetlink.Header{
|
||||
Command: unix.ETHTOOL_MSG_LINKMODES_SET,
|
||||
Version: unix.ETHTOOL_GENL_VERSION,
|
||||
},
|
||||
Data: b,
|
||||
},
|
||||
f.ID,
|
||||
netlink.Request|netlink.Acknowledge,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// setLinkAttrs sets up link attributes that can be queried by external tools.
|
||||
// Its failure is non-fatal to interface bringup.
|
||||
func setLinkAttrs(iface tun.Device) error {
|
||||
// By default the link speed is 10Mbps, which is easily exceeded and causes monitoring tools to complain (#3933).
|
||||
return setLinkSpeed(iface, unix.SPEED_UNKNOWN)
|
||||
}
|
||||
|
||||
@@ -53,9 +53,6 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
|
||||
dev.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
if err := setLinkAttrs(dev); err != nil {
|
||||
logf("setting link attributes: %v", err)
|
||||
}
|
||||
name, err := interfaceName(dev)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
|
||||
@@ -22,10 +22,7 @@ import (
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"go4.org/mem"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"tailscale.com/disco"
|
||||
tsmetrics "tailscale.com/metrics"
|
||||
"tailscale.com/net/connstats"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/packet/checksum"
|
||||
"tailscale.com/net/tsaddr"
|
||||
@@ -35,10 +32,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/usermetric"
|
||||
"tailscale.com/wgengine/capture"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/netstack/gro"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
@@ -77,16 +71,6 @@ var parsedPacketPool = sync.Pool{New: func() any { return new(packet.Parsed) }}
|
||||
// It must not hold onto the packet struct, as its backing storage will be reused.
|
||||
type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
|
||||
|
||||
// GROFilterFunc is a FilterFunc extended with a *gro.GRO, enabling increased
|
||||
// throughput where GRO is supported by a packet.Parsed interceptor, e.g.
|
||||
// netstack/gVisor, and we are handling a vector of packets. Callers must pass a
|
||||
// nil g for the first packet in a given vector, and continue passing the
|
||||
// returned *gro.GRO for all remaining packets in said vector. If the returned
|
||||
// *gro.GRO is non-nil after the last packet for a given vector is passed
|
||||
// through the GROFilterFunc, the caller must also call Flush() on it to deliver
|
||||
// any previously Enqueue()'d packets.
|
||||
type GROFilterFunc func(p *packet.Parsed, w *Wrapper, g *gro.GRO) (filter.Response, *gro.GRO)
|
||||
|
||||
// Wrapper augments a tun.Device with packet filtering and injection.
|
||||
//
|
||||
// A Wrapper starts in a "corked" mode where Read calls are blocked
|
||||
@@ -171,13 +155,7 @@ type Wrapper struct {
|
||||
// PreFilterPacketInboundFromWireGuard is the inbound filter function that runs before the main filter
|
||||
// and therefore sees the packets that may be later dropped by it.
|
||||
PreFilterPacketInboundFromWireGuard FilterFunc
|
||||
// PostFilterPacketInboundFromWireGuard is the inbound filter function that runs after the main filter.
|
||||
PostFilterPacketInboundFromWireGuard GROFilterFunc
|
||||
// PreFilterPacketOutboundToWireGuardNetstackIntercept is a filter function that runs before the main filter
|
||||
// for packets from the local system. This filter is populated by netstack to hook
|
||||
// packets that should be handled by netstack. If set, this filter runs before
|
||||
// PreFilterFromTunToEngine.
|
||||
PreFilterPacketOutboundToWireGuardNetstackIntercept GROFilterFunc
|
||||
|
||||
// PreFilterPacketOutboundToWireGuardEngineIntercept is a filter function that runs before the main filter
|
||||
// for packets from the local system. This filter is populated by wgengine to hook
|
||||
// packets which it handles internally. If both this and PreFilterFromTunToNetstack
|
||||
@@ -186,9 +164,6 @@ type Wrapper struct {
|
||||
// PostFilterPacketOutboundToWireGuard is the outbound filter function that runs after the main filter.
|
||||
PostFilterPacketOutboundToWireGuard FilterFunc
|
||||
|
||||
// OnTSMPPongReceived, if non-nil, is called whenever a TSMP pong arrives.
|
||||
OnTSMPPongReceived func(packet.TSMPPongReply)
|
||||
|
||||
// OnICMPEchoResponseReceived, if non-nil, is called whenever a ICMP echo response
|
||||
// arrives. If the packet is to be handled internally this returns true,
|
||||
// false otherwise.
|
||||
@@ -203,33 +178,11 @@ type Wrapper struct {
|
||||
|
||||
// disableTSMPRejected disables TSMP rejected responses. For tests.
|
||||
disableTSMPRejected bool
|
||||
|
||||
// stats maintains per-connection counters.
|
||||
stats atomic.Pointer[connstats.Statistics]
|
||||
|
||||
captureHook syncs.AtomicValue[capture.Callback]
|
||||
|
||||
metrics *metrics
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
inboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[usermetric.DropLabels]
|
||||
outboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[usermetric.DropLabels]
|
||||
}
|
||||
|
||||
func registerMetrics(reg *usermetric.Registry) *metrics {
|
||||
return &metrics{
|
||||
inboundDroppedPacketsTotal: reg.DroppedPacketsInbound(),
|
||||
outboundDroppedPacketsTotal: reg.DroppedPacketsOutbound(),
|
||||
}
|
||||
}
|
||||
|
||||
// tunInjectedRead is an injected packet pretending to be a tun.Read().
|
||||
type tunInjectedRead struct {
|
||||
// Only one of packet or data should be set, and are read in that order of
|
||||
// precedence.
|
||||
packet *stack.PacketBuffer
|
||||
data []byte
|
||||
data []byte
|
||||
}
|
||||
|
||||
// tunVectorReadResult is the result of a tun.Read(), or an injected packet
|
||||
@@ -255,21 +208,22 @@ func (w *Wrapper) Start() {
|
||||
close(w.startCh)
|
||||
}
|
||||
|
||||
func WrapTAP(logf logger.Logf, tdev tun.Device, m *usermetric.Registry) *Wrapper {
|
||||
return wrap(logf, tdev, true, m)
|
||||
func WrapTAP(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
return wrap(logf, tdev, true)
|
||||
}
|
||||
|
||||
func Wrap(logf logger.Logf, tdev tun.Device, m *usermetric.Registry) *Wrapper {
|
||||
return wrap(logf, tdev, false, m)
|
||||
func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
return wrap(logf, tdev, false)
|
||||
}
|
||||
|
||||
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry) *Wrapper {
|
||||
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
|
||||
logf = logger.WithPrefix(logf, "tstun: ")
|
||||
w := &Wrapper{
|
||||
logf: logf,
|
||||
limitedLogf: logger.RateLimitedFn(logf, 1*time.Minute, 2, 10),
|
||||
isTAP: isTAP,
|
||||
tdev: tdev,
|
||||
disableFilter: true, // lanscaping
|
||||
logf: logf,
|
||||
limitedLogf: logger.RateLimitedFn(logf, 1*time.Minute, 2, 10),
|
||||
isTAP: isTAP,
|
||||
tdev: tdev,
|
||||
// bufferConsumed is conceptually a condition variable:
|
||||
// a goroutine should not block when setting it, even with no listeners.
|
||||
bufferConsumed: make(chan struct{}, 1),
|
||||
@@ -281,7 +235,6 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry)
|
||||
// TODO(dmytro): (highly rate-limited) hexdumps should happen on unknown packets.
|
||||
filterFlags: filter.LogAccepts | filter.LogDrops,
|
||||
startCh: make(chan struct{}),
|
||||
metrics: registerMetrics(m),
|
||||
}
|
||||
|
||||
w.vectorBuffer = make([][]byte, tdev.BatchSize())
|
||||
@@ -816,81 +769,6 @@ var (
|
||||
magicDNSIPPortv6 = netip.AddrPortFrom(tsaddr.TailscaleServiceIPv6(), 0)
|
||||
)
|
||||
|
||||
func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConfigTable, gro *gro.GRO) (filter.Response, *gro.GRO) {
|
||||
// Fake ICMP echo responses to MagicDNS (100.100.100.100).
|
||||
if p.IsEchoRequest() {
|
||||
switch p.Dst {
|
||||
case magicDNSIPPort:
|
||||
header := p.ICMP4Header()
|
||||
header.ToResponse()
|
||||
outp := packet.Generate(&header, p.Payload())
|
||||
t.InjectInboundCopy(outp)
|
||||
return filter.DropSilently, gro // don't pass on to OS; already handled
|
||||
case magicDNSIPPortv6:
|
||||
header := p.ICMP6Header()
|
||||
header.ToResponse()
|
||||
outp := packet.Generate(&header, p.Payload())
|
||||
t.InjectInboundCopy(outp)
|
||||
return filter.DropSilently, gro // don't pass on to OS; already handled
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1526 workaround: if we sent disco packets over
|
||||
// Tailscale from ourselves, then drop them, as that shouldn't
|
||||
// happen unless a networking stack is confused, as it seems
|
||||
// macOS in Network Extension mode might be.
|
||||
if p.IPProto == ipproto.UDP && // disco is over UDP; avoid isSelfDisco call for TCP/etc
|
||||
t.isSelfDisco(p) {
|
||||
t.limitedLogf("[unexpected] received self disco out packet over tstun; dropping")
|
||||
metricPacketOutDropSelfDisco.Add(1)
|
||||
return filter.DropSilently, gro
|
||||
}
|
||||
|
||||
if t.PreFilterPacketOutboundToWireGuardNetstackIntercept != nil {
|
||||
var res filter.Response
|
||||
res, gro = t.PreFilterPacketOutboundToWireGuardNetstackIntercept(p, t, gro)
|
||||
if res.IsDrop() {
|
||||
// Handled by netstack.Impl.handleLocalPackets (quad-100 DNS primarily)
|
||||
return res, gro
|
||||
}
|
||||
}
|
||||
if t.PreFilterPacketOutboundToWireGuardEngineIntercept != nil {
|
||||
if res := t.PreFilterPacketOutboundToWireGuardEngineIntercept(p, t); res.IsDrop() {
|
||||
// Handled by userspaceEngine.handleLocalPackets (primarily handles
|
||||
// quad-100 if netstack is not installed).
|
||||
return res, gro
|
||||
}
|
||||
}
|
||||
|
||||
// If the outbound packet is to a jailed peer, use our jailed peer
|
||||
// packet filter.
|
||||
var filt *filter.Filter
|
||||
if pc.outboundPacketIsJailed(p) {
|
||||
filt = t.jailedFilter.Load()
|
||||
} else {
|
||||
filt = t.filter.Load()
|
||||
}
|
||||
if filt == nil {
|
||||
return filter.Drop, gro
|
||||
}
|
||||
|
||||
if filt.RunOut(p, t.filterFlags) != filter.Accept {
|
||||
metricPacketOutDropFilter.Add(1)
|
||||
// TODO(#14280): increment a t.metrics.outboundDroppedPacketsTotal here
|
||||
// once we figure out & document what labels to use for multicast,
|
||||
// link-local-unicast, IP fragments, etc. But they're not
|
||||
// usermetric.ReasonACL.
|
||||
return filter.Drop, gro
|
||||
}
|
||||
|
||||
if t.PostFilterPacketOutboundToWireGuard != nil {
|
||||
if res := t.PostFilterPacketOutboundToWireGuard(p, t); res.IsDrop() {
|
||||
return res, gro
|
||||
}
|
||||
}
|
||||
return filter.Accept, gro
|
||||
}
|
||||
|
||||
// noteActivity records that there was a read or write at the current time.
|
||||
func (t *Wrapper) noteActivity() {
|
||||
t.lastActivityAtomic.StoreAtomic(mono.Now())
|
||||
@@ -917,7 +795,7 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
return 0, res.err
|
||||
}
|
||||
if res.data == nil {
|
||||
return t.injectedRead(res.injected, buffs, sizes, offset)
|
||||
panic("unreachable; lanscaping")
|
||||
}
|
||||
|
||||
metricPacketOut.Add(int64(len(res.data)))
|
||||
@@ -925,9 +803,7 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
var buffsPos int
|
||||
p := parsedPacketPool.Get().(*packet.Parsed)
|
||||
defer parsedPacketPool.Put(p)
|
||||
captHook := t.captureHook.Load()
|
||||
pc := t.peerConfig.Load()
|
||||
var buffsGRO *gro.GRO
|
||||
for _, data := range res.data {
|
||||
p.Decode(data[res.dataOffset:])
|
||||
|
||||
@@ -936,17 +812,6 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
if captHook != nil {
|
||||
captHook(capture.FromLocal, t.now(), p.Buffer(), p.CaptureMeta)
|
||||
}
|
||||
if !t.disableFilter {
|
||||
var response filter.Response
|
||||
response, buffsGRO = t.filterPacketOutboundToWireGuard(p, pc, buffsGRO)
|
||||
if response != filter.Accept {
|
||||
metricPacketOutDrop.Add(1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to do SNAT after filtering, so that any flow tracking in
|
||||
// the filter sees the original source address. See #12133.
|
||||
@@ -956,14 +821,8 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
panic(fmt.Sprintf("short copy: %d != %d", n, len(data)-res.dataOffset))
|
||||
}
|
||||
sizes[buffsPos] = n
|
||||
if stats := t.stats.Load(); stats != nil {
|
||||
stats.UpdateTxVirtual(p.Buffer())
|
||||
}
|
||||
buffsPos++
|
||||
}
|
||||
if buffsGRO != nil {
|
||||
buffsGRO.Flush()
|
||||
}
|
||||
|
||||
// t.vectorBuffer has a fixed location in memory.
|
||||
// TODO(raggi): add an explicit field and possibly method to the tunVectorReadResult
|
||||
@@ -981,214 +840,6 @@ const (
|
||||
minTCPHeaderSize = 20
|
||||
)
|
||||
|
||||
func stackGSOToTunGSO(pkt []byte, gso stack.GSO) (tun.GSOOptions, error) {
|
||||
options := tun.GSOOptions{
|
||||
CsumStart: gso.L3HdrLen,
|
||||
CsumOffset: gso.CsumOffset,
|
||||
GSOSize: gso.MSS,
|
||||
NeedsCsum: gso.NeedsCsum,
|
||||
}
|
||||
switch gso.Type {
|
||||
case stack.GSONone:
|
||||
options.GSOType = tun.GSONone
|
||||
return options, nil
|
||||
case stack.GSOTCPv4:
|
||||
options.GSOType = tun.GSOTCPv4
|
||||
case stack.GSOTCPv6:
|
||||
options.GSOType = tun.GSOTCPv6
|
||||
default:
|
||||
return tun.GSOOptions{}, fmt.Errorf("unsupported gVisor GSOType: %v", gso.Type)
|
||||
}
|
||||
// options.HdrLen is both layer 3 and 4 together, whereas gVisor only
|
||||
// gives us layer 3 length. We have to gather TCP header length
|
||||
// ourselves.
|
||||
if len(pkt) < int(gso.L3HdrLen)+minTCPHeaderSize {
|
||||
return tun.GSOOptions{}, errors.New("gVisor GSOTCP packet length too short")
|
||||
}
|
||||
tcphLen := uint16(pkt[int(gso.L3HdrLen)+12] >> 4 * 4)
|
||||
options.HdrLen = gso.L3HdrLen + tcphLen
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// invertGSOChecksum inverts the transport layer checksum in pkt if gVisor
|
||||
// handed us a segment with a partial checksum. A partial checksum is not a
|
||||
// ones' complement of the sum, and incremental checksum updating is not yet
|
||||
// partial checksum aware. This may be called twice for a single packet,
|
||||
// both before and after partial checksum updates where later checksum
|
||||
// offloading still expects a partial checksum.
|
||||
// TODO(jwhited): plumb partial checksum awareness into net/packet/checksum.
|
||||
func invertGSOChecksum(pkt []byte, gso stack.GSO) {
|
||||
if gso.NeedsCsum != true {
|
||||
return
|
||||
}
|
||||
at := int(gso.L3HdrLen + gso.CsumOffset)
|
||||
if at+1 > len(pkt)-1 {
|
||||
return
|
||||
}
|
||||
pkt[at] = ^pkt[at]
|
||||
pkt[at+1] = ^pkt[at+1]
|
||||
}
|
||||
|
||||
// injectedRead handles injected reads, which bypass filters.
|
||||
func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []int, offset int) (n int, err error) {
|
||||
var gso stack.GSO
|
||||
|
||||
pkt := outBuffs[0][offset:]
|
||||
if res.packet != nil {
|
||||
bufN := copy(pkt, res.packet.NetworkHeader().Slice())
|
||||
bufN += copy(pkt[bufN:], res.packet.TransportHeader().Slice())
|
||||
bufN += copy(pkt[bufN:], res.packet.Data().AsRange().ToSlice())
|
||||
gso = res.packet.GSOOptions
|
||||
pkt = pkt[:bufN]
|
||||
defer res.packet.DecRef() // defer DecRef so we may continue to reference it
|
||||
} else {
|
||||
sizes[0] = copy(pkt, res.data)
|
||||
pkt = pkt[:sizes[0]]
|
||||
n = 1
|
||||
}
|
||||
|
||||
pc := t.peerConfig.Load()
|
||||
|
||||
p := parsedPacketPool.Get().(*packet.Parsed)
|
||||
defer parsedPacketPool.Put(p)
|
||||
p.Decode(pkt)
|
||||
|
||||
invertGSOChecksum(pkt, gso)
|
||||
pc.snat(p)
|
||||
invertGSOChecksum(pkt, gso)
|
||||
|
||||
if m := t.destIPActivity.Load(); m != nil {
|
||||
if fn := m[p.Dst.Addr()]; fn != nil {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
if res.packet != nil {
|
||||
var gsoOptions tun.GSOOptions
|
||||
gsoOptions, err = stackGSOToTunGSO(pkt, gso)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err = tun.GSOSplit(pkt, gsoOptions, outBuffs, sizes, offset)
|
||||
}
|
||||
|
||||
if stats := t.stats.Load(); stats != nil {
|
||||
for i := 0; i < n; i++ {
|
||||
stats.UpdateTxVirtual(outBuffs[i][offset : offset+sizes[i]])
|
||||
}
|
||||
}
|
||||
|
||||
t.noteActivity()
|
||||
metricPacketOut.Add(int64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook capture.Callback, pc *peerConfigTable, gro *gro.GRO) (filter.Response, *gro.GRO) {
|
||||
if captHook != nil {
|
||||
captHook(capture.FromPeer, t.now(), p.Buffer(), p.CaptureMeta)
|
||||
}
|
||||
|
||||
if p.IPProto == ipproto.TSMP {
|
||||
if pingReq, ok := p.AsTSMPPing(); ok {
|
||||
t.noteActivity()
|
||||
t.injectOutboundPong(p, pingReq)
|
||||
return filter.DropSilently, gro
|
||||
} else if data, ok := p.AsTSMPPong(); ok {
|
||||
if f := t.OnTSMPPongReceived; f != nil {
|
||||
f(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.IsEchoResponse() {
|
||||
if f := t.OnICMPEchoResponseReceived; f != nil && f(p) {
|
||||
// Note: this looks dropped in metrics, even though it was
|
||||
// handled internally.
|
||||
return filter.DropSilently, gro
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1526 workaround: if we see disco packets over
|
||||
// Tailscale from ourselves, then drop them, as that shouldn't
|
||||
// happen unless a networking stack is confused, as it seems
|
||||
// macOS in Network Extension mode might be.
|
||||
if p.IPProto == ipproto.UDP && // disco is over UDP; avoid isSelfDisco call for TCP/etc
|
||||
t.isSelfDisco(p) {
|
||||
t.limitedLogf("[unexpected] received self disco in packet over tstun; dropping")
|
||||
metricPacketInDropSelfDisco.Add(1)
|
||||
return filter.DropSilently, gro
|
||||
}
|
||||
|
||||
if t.PreFilterPacketInboundFromWireGuard != nil {
|
||||
if res := t.PreFilterPacketInboundFromWireGuard(p, t); res.IsDrop() {
|
||||
return res, gro
|
||||
}
|
||||
}
|
||||
|
||||
var filt *filter.Filter
|
||||
if pc.inboundPacketIsJailed(p) {
|
||||
filt = t.jailedFilter.Load()
|
||||
} else {
|
||||
filt = t.filter.Load()
|
||||
}
|
||||
if filt == nil {
|
||||
return filter.Drop, gro
|
||||
}
|
||||
outcome := filt.RunIn(p, t.filterFlags)
|
||||
|
||||
// Let peerapi through the filter; its ACLs are handled at L7,
|
||||
// not at the packet level.
|
||||
if outcome != filter.Accept &&
|
||||
p.IPProto == ipproto.TCP &&
|
||||
p.TCPFlags&packet.TCPSyn != 0 &&
|
||||
t.PeerAPIPort != nil {
|
||||
if port, ok := t.PeerAPIPort(p.Dst.Addr()); ok && port == p.Dst.Port() {
|
||||
outcome = filter.Accept
|
||||
}
|
||||
}
|
||||
|
||||
if outcome != filter.Accept {
|
||||
metricPacketInDropFilter.Add(1)
|
||||
t.metrics.inboundDroppedPacketsTotal.Add(usermetric.DropLabels{
|
||||
Reason: usermetric.ReasonACL,
|
||||
}, 1)
|
||||
|
||||
// Tell them, via TSMP, we're dropping them due to the ACL.
|
||||
// Their host networking stack can translate this into ICMP
|
||||
// or whatnot as required. But notably, their GUI or tailscale CLI
|
||||
// can show them a rejection history with reasons.
|
||||
if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 && !t.disableTSMPRejected {
|
||||
rj := packet.TailscaleRejectedHeader{
|
||||
IPSrc: p.Dst.Addr(),
|
||||
IPDst: p.Src.Addr(),
|
||||
Src: p.Src,
|
||||
Dst: p.Dst,
|
||||
Proto: p.IPProto,
|
||||
Reason: packet.RejectedDueToACLs,
|
||||
}
|
||||
if filt.ShieldsUp() {
|
||||
rj.Reason = packet.RejectedDueToShieldsUp
|
||||
}
|
||||
pkt := packet.Generate(rj, nil)
|
||||
t.InjectOutbound(pkt)
|
||||
|
||||
// TODO(bradfitz): also send a TCP RST, after the TSMP message.
|
||||
}
|
||||
|
||||
return filter.Drop, gro
|
||||
}
|
||||
|
||||
if t.PostFilterPacketInboundFromWireGuard != nil {
|
||||
var res filter.Response
|
||||
res, gro = t.PostFilterPacketInboundFromWireGuard(p, t, gro)
|
||||
if res.IsDrop() {
|
||||
return res, gro
|
||||
}
|
||||
}
|
||||
|
||||
return filter.Accept, gro
|
||||
}
|
||||
|
||||
// Write accepts incoming packets. The packets begin at buffs[:][offset:],
|
||||
// like wireguard-go/tun.Device.Write. Write is called per-peer via
|
||||
// wireguard-go/device.Peer.RoutineSequentialReceiver, so it MUST be
|
||||
@@ -1198,28 +849,10 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
|
||||
i := 0
|
||||
p := parsedPacketPool.Get().(*packet.Parsed)
|
||||
defer parsedPacketPool.Put(p)
|
||||
captHook := t.captureHook.Load()
|
||||
pc := t.peerConfig.Load()
|
||||
var buffsGRO *gro.GRO
|
||||
for _, buff := range buffs {
|
||||
p.Decode(buff[offset:])
|
||||
pc.dnat(p)
|
||||
if !t.disableFilter {
|
||||
var res filter.Response
|
||||
// TODO(jwhited): name and document this filter code path
|
||||
// appropriately. It is not only responsible for filtering, it
|
||||
// also routes packets towards gVisor/netstack.
|
||||
res, buffsGRO = t.filterPacketInboundFromWireGuard(p, captHook, pc, buffsGRO)
|
||||
if res != filter.Accept {
|
||||
metricPacketInDrop.Add(1)
|
||||
} else {
|
||||
buffs[i] = buff
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
if buffsGRO != nil {
|
||||
buffsGRO.Flush()
|
||||
}
|
||||
if t.disableFilter {
|
||||
i = len(buffs)
|
||||
@@ -1230,9 +863,6 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
|
||||
t.noteActivity()
|
||||
_, err := t.tdevWrite(buffs, offset)
|
||||
if err != nil {
|
||||
t.metrics.inboundDroppedPacketsTotal.Add(usermetric.DropLabels{
|
||||
Reason: usermetric.ReasonError,
|
||||
}, int64(len(buffs)))
|
||||
}
|
||||
return len(buffs), err
|
||||
}
|
||||
@@ -1240,11 +870,6 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
|
||||
}
|
||||
|
||||
func (t *Wrapper) tdevWrite(buffs [][]byte, offset int) (int, error) {
|
||||
if stats := t.stats.Load(); stats != nil {
|
||||
for i := range buffs {
|
||||
stats.UpdateRxVirtual((buffs)[i][offset:])
|
||||
}
|
||||
}
|
||||
return t.tdev.Write(buffs, offset)
|
||||
}
|
||||
|
||||
@@ -1264,76 +889,6 @@ func (t *Wrapper) SetJailedFilter(filt *filter.Filter) {
|
||||
t.jailedFilter.Store(filt)
|
||||
}
|
||||
|
||||
// InjectInboundPacketBuffer makes the Wrapper device behave as if a packet
|
||||
// (pkt) with the given contents was received from the network.
|
||||
// It takes ownership of one reference count on pkt. The injected
|
||||
// packet will not pass through inbound filters.
|
||||
//
|
||||
// pkt will be copied into buffs before writing to the underlying tun.Device.
|
||||
// Therefore, callers must allocate and pass a buffs slice that is sized
|
||||
// appropriately for holding pkt.Size() + PacketStartOffset as a single
|
||||
// element (buffs[0]) and split across multiple elements if the originating
|
||||
// stack supports GSO. sizes must be sized with similar consideration,
|
||||
// len(buffs) should be equal to len(sizes). If any len(buffs[<index>]) was
|
||||
// mutated by InjectInboundPacketBuffer it will be reset to cap(buffs[<index>])
|
||||
// before returning.
|
||||
//
|
||||
// This path is typically used to deliver synthesized packets to the
|
||||
// host networking stack.
|
||||
func (t *Wrapper) InjectInboundPacketBuffer(pkt *stack.PacketBuffer, buffs [][]byte, sizes []int) error {
|
||||
buf := buffs[0][PacketStartOffset:]
|
||||
|
||||
bufN := copy(buf, pkt.NetworkHeader().Slice())
|
||||
bufN += copy(buf[bufN:], pkt.TransportHeader().Slice())
|
||||
bufN += copy(buf[bufN:], pkt.Data().AsRange().ToSlice())
|
||||
if bufN != pkt.Size() {
|
||||
panic("unexpected packet size after copy")
|
||||
}
|
||||
buf = buf[:bufN]
|
||||
defer pkt.DecRef()
|
||||
|
||||
pc := t.peerConfig.Load()
|
||||
|
||||
p := parsedPacketPool.Get().(*packet.Parsed)
|
||||
defer parsedPacketPool.Put(p)
|
||||
p.Decode(buf)
|
||||
captHook := t.captureHook.Load()
|
||||
if captHook != nil {
|
||||
captHook(capture.SynthesizedToLocal, t.now(), p.Buffer(), p.CaptureMeta)
|
||||
}
|
||||
|
||||
invertGSOChecksum(buf, pkt.GSOOptions)
|
||||
pc.dnat(p)
|
||||
invertGSOChecksum(buf, pkt.GSOOptions)
|
||||
|
||||
gso, err := stackGSOToTunGSO(buf, pkt.GSOOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(jwhited): support GSO passthrough to t.tdev. If t.tdev supports
|
||||
// GSO we don't need to split here and coalesce inside wireguard-go,
|
||||
// we can pass a coalesced segment all the way through.
|
||||
n, err := tun.GSOSplit(buf, gso, buffs, sizes, PacketStartOffset)
|
||||
if err != nil {
|
||||
if errors.Is(err, tun.ErrTooManySegments) {
|
||||
t.limitedLogf("InjectInboundPacketBuffer: GSO split overflows buffs")
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
buffs[i] = buffs[i][:PacketStartOffset+sizes[i]]
|
||||
}
|
||||
defer func() {
|
||||
for i := 0; i < n; i++ {
|
||||
buffs[i] = buffs[i][:cap(buffs[i])]
|
||||
}
|
||||
}()
|
||||
_, err = t.tdevWrite(buffs[:n], PacketStartOffset)
|
||||
return err
|
||||
}
|
||||
|
||||
// InjectInboundDirect makes the Wrapper device behave as if a packet
|
||||
// with the given contents was received from the network.
|
||||
// It blocks and does not take ownership of the packet.
|
||||
@@ -1358,48 +913,6 @@ func (t *Wrapper) InjectInboundDirect(buf []byte, offset int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// InjectInboundCopy takes a packet without leading space,
|
||||
// reallocates it to conform to the InjectInboundDirect interface
|
||||
// and calls InjectInboundDirect on it. Injecting a nil packet is a no-op.
|
||||
func (t *Wrapper) InjectInboundCopy(packet []byte) error {
|
||||
// We duplicate this check from InjectInboundDirect here
|
||||
// to avoid wasting an allocation on an oversized packet.
|
||||
if len(packet) > MaxPacketSize {
|
||||
return errPacketTooBig
|
||||
}
|
||||
if len(packet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := make([]byte, PacketStartOffset+len(packet))
|
||||
copy(buf[PacketStartOffset:], packet)
|
||||
|
||||
return t.InjectInboundDirect(buf, PacketStartOffset)
|
||||
}
|
||||
|
||||
func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingRequest) {
|
||||
pong := packet.TSMPPongReply{
|
||||
Data: req.Data,
|
||||
}
|
||||
if t.PeerAPIPort != nil {
|
||||
pong.PeerAPIPort, _ = t.PeerAPIPort(pp.Dst.Addr())
|
||||
}
|
||||
switch pp.IPVersion {
|
||||
case 4:
|
||||
h4 := pp.IP4Header()
|
||||
h4.ToResponse()
|
||||
pong.IPHeader = h4
|
||||
case 6:
|
||||
h6 := pp.IP6Header()
|
||||
h6.ToResponse()
|
||||
pong.IPHeader = h6
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
t.InjectOutbound(packet.Generate(pong, nil))
|
||||
}
|
||||
|
||||
// InjectOutbound makes the Wrapper device behave as if a packet
|
||||
// with the given contents was sent to the network.
|
||||
// It does not block, but takes ownership of the packet.
|
||||
@@ -1416,28 +929,6 @@ func (t *Wrapper) InjectOutbound(pkt []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InjectOutboundPacketBuffer logically behaves as InjectOutbound. It takes ownership of one
|
||||
// reference count on the packet, and the packet may be mutated. The packet refcount will be
|
||||
// decremented after the injected buffer has been read.
|
||||
func (t *Wrapper) InjectOutboundPacketBuffer(pkt *stack.PacketBuffer) error {
|
||||
size := pkt.Size()
|
||||
if size > MaxPacketSize {
|
||||
pkt.DecRef()
|
||||
return errPacketTooBig
|
||||
}
|
||||
if size == 0 {
|
||||
pkt.DecRef()
|
||||
return nil
|
||||
}
|
||||
if capt := t.captureHook.Load(); capt != nil {
|
||||
b := pkt.ToBuffer()
|
||||
capt(capture.SynthesizedToPeer, t.now(), b.Flatten(), packet.CaptureMeta{})
|
||||
}
|
||||
|
||||
t.injectOutbound(tunInjectedRead{packet: pkt})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Wrapper) BatchSize() int {
|
||||
if runtime.GOOS == "linux" {
|
||||
// Always setup Linux to handle vectors, even in the very rare case that
|
||||
@@ -1455,12 +946,6 @@ func (t *Wrapper) Unwrap() tun.Device {
|
||||
return t.tdev
|
||||
}
|
||||
|
||||
// SetStatistics specifies a per-connection statistics aggregator.
|
||||
// Nil may be specified to disable statistics gathering.
|
||||
func (t *Wrapper) SetStatistics(stats *connstats.Statistics) {
|
||||
t.stats.Store(stats)
|
||||
}
|
||||
|
||||
var (
|
||||
metricPacketIn = clientmetric.NewCounter("tstun_in_from_wg")
|
||||
metricPacketInDrop = clientmetric.NewCounter("tstun_in_from_wg_drop")
|
||||
@@ -1472,7 +957,3 @@ var (
|
||||
metricPacketOutDropFilter = clientmetric.NewCounter("tstun_out_to_wg_drop_filter")
|
||||
metricPacketOutDropSelfDisco = clientmetric.NewCounter("tstun_out_to_wg_drop_self_disco")
|
||||
)
|
||||
|
||||
func (t *Wrapper) InstallCaptureHook(cb capture.Callback) {
|
||||
t.captureHook.Store(cb)
|
||||
}
|
||||
|
||||
@@ -4,17 +4,7 @@
|
||||
package tstun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
// SetLinkFeaturesPostUp configures link features on t based on select TS_TUN_
|
||||
@@ -24,59 +14,4 @@ func (t *Wrapper) SetLinkFeaturesPostUp() {
|
||||
if t.isTAP || runtime.GOOS == "android" {
|
||||
return
|
||||
}
|
||||
if groDev, ok := t.tdev.(tun.GRODevice); ok {
|
||||
if envknob.Bool("TS_TUN_DISABLE_UDP_GRO") {
|
||||
groDev.DisableUDPGRO()
|
||||
}
|
||||
if envknob.Bool("TS_TUN_DISABLE_TCP_GRO") {
|
||||
groDev.DisableTCPGRO()
|
||||
}
|
||||
err := probeTCPGRO(groDev)
|
||||
if errors.Is(err, unix.EINVAL) {
|
||||
groDev.DisableTCPGRO()
|
||||
groDev.DisableUDPGRO()
|
||||
t.logf("disabled TUN TCP & UDP GRO due to GRO probe error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func probeTCPGRO(dev tun.GRODevice) error {
|
||||
ipPort := netip.MustParseAddrPort(tsaddr.TailscaleServiceIPString + ":0")
|
||||
fingerprint := []byte("tailscale-probe-tun-gro")
|
||||
segmentSize := len(fingerprint)
|
||||
iphLen := 20
|
||||
tcphLen := 20
|
||||
totalLen := iphLen + tcphLen + segmentSize
|
||||
ipAs4 := ipPort.Addr().As4()
|
||||
bufs := make([][]byte, 2)
|
||||
for i := range bufs {
|
||||
bufs[i] = make([]byte, PacketStartOffset+totalLen, PacketStartOffset+(totalLen*2))
|
||||
ipv4H := header.IPv4(bufs[i][PacketStartOffset:])
|
||||
ipv4H.Encode(&header.IPv4Fields{
|
||||
SrcAddr: tcpip.AddrFromSlice(ipAs4[:]),
|
||||
DstAddr: tcpip.AddrFromSlice(ipAs4[:]),
|
||||
Protocol: unix.IPPROTO_TCP,
|
||||
// Use a zero value TTL as best effort means to reduce chance of
|
||||
// probe packet leaking further than it needs to.
|
||||
TTL: 0,
|
||||
TotalLength: uint16(totalLen),
|
||||
})
|
||||
tcpH := header.TCP(bufs[i][PacketStartOffset+iphLen:])
|
||||
tcpH.Encode(&header.TCPFields{
|
||||
SrcPort: ipPort.Port(),
|
||||
DstPort: ipPort.Port(),
|
||||
SeqNum: 1 + uint32(i*segmentSize),
|
||||
AckNum: 1,
|
||||
DataOffset: 20,
|
||||
Flags: header.TCPFlagAck,
|
||||
WindowSize: 3000,
|
||||
})
|
||||
copy(bufs[i][PacketStartOffset+iphLen+tcphLen:], fingerprint)
|
||||
ipv4H.SetChecksum(^ipv4H.CalculateChecksum())
|
||||
pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(tcphLen+segmentSize))
|
||||
pseudoCsum = checksum.Checksum(bufs[i][PacketStartOffset+iphLen+tcphLen:], pseudoCsum)
|
||||
tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum))
|
||||
}
|
||||
_, err := dev.Write(bufs, PacketStartOffset)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -22,13 +22,10 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"go4.org/mem"
|
||||
"go4.org/netipx"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/net/connstats"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstime/mono"
|
||||
@@ -521,124 +518,6 @@ func TestAtomic64Alignment(t *testing.T) {
|
||||
c.lastActivityAtomic.StoreAtomic(mono.Now())
|
||||
}
|
||||
|
||||
func TestPeerAPIBypass(t *testing.T) {
|
||||
reg := new(usermetric.Registry)
|
||||
wrapperWithPeerAPI := &Wrapper{
|
||||
PeerAPIPort: func(ip netip.Addr) (port uint16, ok bool) {
|
||||
if ip == netip.MustParseAddr("100.64.1.2") {
|
||||
return 60000, true
|
||||
}
|
||||
return
|
||||
},
|
||||
metrics: registerMetrics(reg),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
w *Wrapper
|
||||
filter *filter.Filter
|
||||
pkt []byte
|
||||
want filter.Response
|
||||
}{
|
||||
{
|
||||
name: "reject_nil_filter",
|
||||
w: &Wrapper{
|
||||
PeerAPIPort: func(netip.Addr) (port uint16, ok bool) {
|
||||
return 60000, true
|
||||
},
|
||||
metrics: registerMetrics(reg),
|
||||
},
|
||||
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
|
||||
want: filter.Drop,
|
||||
},
|
||||
{
|
||||
name: "reject_with_filter",
|
||||
w: &Wrapper{
|
||||
metrics: registerMetrics(reg),
|
||||
},
|
||||
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
|
||||
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
|
||||
want: filter.Drop,
|
||||
},
|
||||
{
|
||||
name: "peerapi_bypass_filter",
|
||||
w: wrapperWithPeerAPI,
|
||||
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
|
||||
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
|
||||
want: filter.Accept,
|
||||
},
|
||||
{
|
||||
name: "peerapi_dont_bypass_filter_wrong_port",
|
||||
w: wrapperWithPeerAPI,
|
||||
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
|
||||
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60001),
|
||||
want: filter.Drop,
|
||||
},
|
||||
{
|
||||
name: "peerapi_dont_bypass_filter_wrong_dst_ip",
|
||||
w: wrapperWithPeerAPI,
|
||||
filter: filter.NewAllowNone(logger.Discard, new(netipx.IPSet)),
|
||||
pkt: tcp4syn("1.2.3.4", "100.64.1.3", 1234, 60000),
|
||||
want: filter.Drop,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := new(packet.Parsed)
|
||||
p.Decode(tt.pkt)
|
||||
tt.w.SetFilter(tt.filter)
|
||||
tt.w.disableTSMPRejected = true
|
||||
tt.w.logf = t.Logf
|
||||
if got, _ := tt.w.filterPacketInboundFromWireGuard(p, nil, nil, nil); got != tt.want {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1526: drop disco frames from ourselves.
|
||||
func TestFilterDiscoLoop(t *testing.T) {
|
||||
var memLog tstest.MemLogger
|
||||
discoPub := key.DiscoPublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 31: 0}))
|
||||
tw := &Wrapper{logf: memLog.Logf, limitedLogf: memLog.Logf}
|
||||
tw.SetDiscoKey(discoPub)
|
||||
uh := packet.UDP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
IPProto: ipproto.UDP,
|
||||
Src: netaddr.IPv4(1, 2, 3, 4),
|
||||
Dst: netaddr.IPv4(5, 6, 7, 8),
|
||||
},
|
||||
SrcPort: 9,
|
||||
DstPort: 10,
|
||||
}
|
||||
discobs := discoPub.Raw32()
|
||||
discoPayload := fmt.Sprintf("%s%s%s", disco.Magic, discobs[:], [disco.NonceLen]byte{})
|
||||
pkt := make([]byte, uh.Len()+len(discoPayload))
|
||||
uh.Marshal(pkt)
|
||||
copy(pkt[uh.Len():], discoPayload)
|
||||
|
||||
p := new(packet.Parsed)
|
||||
p.Decode(pkt)
|
||||
got, _ := tw.filterPacketInboundFromWireGuard(p, nil, nil, nil)
|
||||
if got != filter.DropSilently {
|
||||
t.Errorf("got %v; want DropSilently", got)
|
||||
}
|
||||
if got, want := memLog.String(), "[unexpected] received self disco in packet over tstun; dropping\n"; got != want {
|
||||
t.Errorf("log output mismatch\n got: %q\nwant: %q\n", got, want)
|
||||
}
|
||||
|
||||
memLog.Reset()
|
||||
pp := new(packet.Parsed)
|
||||
pp.Decode(pkt)
|
||||
got, _ = tw.filterPacketOutboundToWireGuard(pp, nil, nil)
|
||||
if got != filter.DropSilently {
|
||||
t.Errorf("got %v; want DropSilently", got)
|
||||
}
|
||||
if got, want := memLog.String(), "[unexpected] received self disco out packet over tstun; dropping\n"; got != want {
|
||||
t.Errorf("log output mismatch\n got: %q\nwant: %q\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(andrew-d): refactor this test to no longer use addrFam, after #11945
|
||||
// removed it in peerConfigFromWGConfig
|
||||
func TestPeerCfg_NAT(t *testing.T) {
|
||||
@@ -893,7 +772,6 @@ func TestCaptureHook(t *testing.T) {
|
||||
w.timeNow = func() time.Time {
|
||||
return now
|
||||
}
|
||||
w.InstallCaptureHook(hook)
|
||||
defer w.Close()
|
||||
|
||||
// Loop reading and discarding packets; this ensures that we don't have
|
||||
|
||||
@@ -5,30 +5,5 @@
|
||||
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ps "github.com/mitchellh/go-ps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tailscaledProcExists = func() bool {
|
||||
procs, err := ps.Processes()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, proc := range procs {
|
||||
name := proc.Executable()
|
||||
const tailscaled = "tailscaled"
|
||||
if len(name) < len(tailscaled) {
|
||||
continue
|
||||
}
|
||||
// Do case insensitive comparison for Windows,
|
||||
// notably, and ignore any ".exe" suffix.
|
||||
if strings.EqualFold(name[:len(tailscaled)], tailscaled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
// the node and the coordination server.
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile --clonefunc
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,ControlDialPlan,Location,UserProfile --clonefunc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -20,11 +19,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
@@ -337,14 +334,13 @@ type Node struct {
|
||||
// Sharer, if non-zero, is the user who shared this node, if different than User.
|
||||
Sharer UserID `json:",omitempty"`
|
||||
|
||||
Key key.NodePublic
|
||||
KeyExpiry time.Time // the zero value if this node does not expire
|
||||
KeySignature tkatype.MarshaledSignature `json:",omitempty"`
|
||||
Machine key.MachinePublic
|
||||
DiscoKey key.DiscoPublic
|
||||
Addresses []netip.Prefix // IP addresses of this Node directly
|
||||
AllowedIPs []netip.Prefix // range of IP addresses to route to this node
|
||||
Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)
|
||||
Key key.NodePublic
|
||||
KeyExpiry time.Time // the zero value if this node does not expire
|
||||
Machine key.MachinePublic
|
||||
DiscoKey key.DiscoPublic
|
||||
Addresses []netip.Prefix // IP addresses of this Node directly
|
||||
AllowedIPs []netip.Prefix // range of IP addresses to route to this node
|
||||
Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)
|
||||
|
||||
// DERP is this node's home DERP region ID integer, but shoved into an
|
||||
// IP:port string for legacy reasons. The IP address is always "127.3.3.40"
|
||||
@@ -484,10 +480,6 @@ type Node struct {
|
||||
// initiate connections, however outbound connections to it should still be
|
||||
// allowed.
|
||||
IsJailed bool `json:",omitempty"`
|
||||
|
||||
// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
|
||||
// node is marked IsWireGuardOnly and being used as an exit node.
|
||||
ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"`
|
||||
}
|
||||
|
||||
// HasCap reports whether the node has the given capability.
|
||||
@@ -1215,7 +1207,6 @@ type RegisterRequest struct {
|
||||
|
||||
NodeKey key.NodePublic
|
||||
OldNodeKey key.NodePublic
|
||||
NLKey key.NLPublic
|
||||
Auth *RegisterResponseAuth `json:",omitempty"`
|
||||
// Expiry optionally specifies the requested key expiry.
|
||||
// The server policy may override.
|
||||
@@ -1230,13 +1221,6 @@ type RegisterRequest struct {
|
||||
// when it stops being active.
|
||||
Ephemeral bool `json:",omitempty"`
|
||||
|
||||
// NodeKeySignature is the node's own node-key signature, re-signed
|
||||
// for its new node key using its network-lock key.
|
||||
//
|
||||
// This field is set when the client retries registration after learning
|
||||
// its NodeKeySignature (which is in need of rotation).
|
||||
NodeKeySignature tkatype.MarshaledSignature
|
||||
|
||||
// The following fields are not used for SignatureNone and are required for
|
||||
// SignatureV1:
|
||||
SignatureType SignatureType `json:",omitempty"`
|
||||
@@ -1264,10 +1248,6 @@ type RegisterResponse struct {
|
||||
MachineAuthorized bool // TODO(crawshaw): move to using MachineStatus
|
||||
AuthURL string // if set, authorization pending
|
||||
|
||||
// If set, this is the current node-key signature that needs to be
|
||||
// re-signed for the node's new node-key.
|
||||
NodeKeySignature tkatype.MarshaledSignature
|
||||
|
||||
// Error indicates that authorization failed. If this is non-empty,
|
||||
// other status fields should be ignored.
|
||||
Error string
|
||||
@@ -1643,93 +1623,7 @@ var FilterAllowAll = []FilterRule{
|
||||
},
|
||||
}
|
||||
|
||||
// DNSConfig is the DNS configuration.
|
||||
type DNSConfig struct {
|
||||
// Resolvers are the DNS resolvers to use, in order of preference.
|
||||
Resolvers []*dnstype.Resolver `json:",omitempty"`
|
||||
|
||||
// Routes maps DNS name suffixes to a set of DNS resolvers to
|
||||
// use. It is used to implement "split DNS" and other advanced DNS
|
||||
// routing overlays.
|
||||
//
|
||||
// Map keys are fully-qualified DNS name suffixes; they may
|
||||
// optionally contain a trailing dot but no leading dot.
|
||||
//
|
||||
// If the value is an empty slice, that means the suffix should still
|
||||
// be handled by Tailscale's built-in resolver (100.100.100.100), such
|
||||
// as for the purpose of handling ExtraRecords.
|
||||
Routes map[string][]*dnstype.Resolver `json:",omitempty"`
|
||||
|
||||
// FallbackResolvers is like Resolvers, but is only used if a
|
||||
// split DNS configuration is requested in a configuration that
|
||||
// doesn't work yet without explicit default resolvers.
|
||||
// https://github.com/tailscale/tailscale/issues/1743
|
||||
FallbackResolvers []*dnstype.Resolver `json:",omitempty"`
|
||||
// Domains are the search domains to use.
|
||||
// Search domains must be FQDNs, but *without* the trailing dot.
|
||||
Domains []string `json:",omitempty"`
|
||||
// Proxied turns on automatic resolution of hostnames for devices
|
||||
// in the network map, aka MagicDNS.
|
||||
// Despite the (legacy) name, does not necessarily cause request
|
||||
// proxying to be enabled.
|
||||
Proxied bool `json:",omitempty"`
|
||||
|
||||
// The following fields are only set and used by
|
||||
// MapRequest.Version >=9 and <14.
|
||||
|
||||
// Nameservers are the IP addresses of the nameservers to use.
|
||||
Nameservers []netip.Addr `json:",omitempty"`
|
||||
|
||||
// CertDomains are the set of DNS names for which the control
|
||||
// plane server will assist with provisioning TLS
|
||||
// certificates. See SetDNSRequest, which can be used to
|
||||
// answer dns-01 ACME challenges for e.g. LetsEncrypt.
|
||||
// These names are FQDNs without trailing periods, and without
|
||||
// any "_acme-challenge." prefix.
|
||||
CertDomains []string `json:",omitempty"`
|
||||
|
||||
// ExtraRecords contains extra DNS records to add to the
|
||||
// MagicDNS config.
|
||||
ExtraRecords []DNSRecord `json:",omitempty"`
|
||||
|
||||
// ExitNodeFilteredSuffixes are the DNS suffixes that the
|
||||
// node, when being an exit node DNS proxy, should not answer.
|
||||
//
|
||||
// The entries do not contain trailing periods and are always
|
||||
// all lowercase.
|
||||
//
|
||||
// If an entry starts with a period, it's a suffix match (but
|
||||
// suffix ".a.b" doesn't match "a.b"; a prefix is required).
|
||||
//
|
||||
// If an entry does not start with a period, it's an exact
|
||||
// match.
|
||||
//
|
||||
// Matches are case insensitive.
|
||||
ExitNodeFilteredSet []string `json:",omitempty"`
|
||||
|
||||
// TempCorpIssue13969 is a temporary (2023-08-16) field for an internal hack day prototype.
|
||||
// It contains a user inputed URL that should have a list of domains to be blocked.
|
||||
// See https://github.com/tailscale/corp/issues/13969.
|
||||
TempCorpIssue13969 string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// DNSRecord is an extra DNS record to add to MagicDNS.
|
||||
type DNSRecord struct {
|
||||
// Name is the fully qualified domain name of
|
||||
// the record to add. The trailing dot is optional.
|
||||
Name string
|
||||
|
||||
// Type is the DNS record type.
|
||||
// Empty means A or AAAA, depending on value.
|
||||
// Other values are currently ignored.
|
||||
Type string `json:",omitempty"`
|
||||
|
||||
// Value is the IP address in string form.
|
||||
// TODO(bradfitz): if we ever add support for record types
|
||||
// with non-UTF8 binary data, add ValueBytes []byte that
|
||||
// would take precedence.
|
||||
Value string
|
||||
}
|
||||
type DNSConfig struct{}
|
||||
|
||||
// PingType is a string representing the kind of ping to perform.
|
||||
type PingType string
|
||||
@@ -2002,23 +1896,9 @@ type MapResponse struct {
|
||||
// to marshal this type using a separate type. See MapResponse docs.
|
||||
Health []string `json:",omitempty"`
|
||||
|
||||
// SSHPolicy, if non-nil, updates the SSH policy for how incoming
|
||||
// SSH connections should be handled.
|
||||
SSHPolicy *SSHPolicy `json:",omitempty"`
|
||||
|
||||
// ControlTime, if non-zero, is the current timestamp according to the control server.
|
||||
ControlTime *time.Time `json:",omitempty"`
|
||||
|
||||
// TKAInfo describes the control plane's view of tailnet
|
||||
// key authority (TKA) state.
|
||||
//
|
||||
// An initial nil TKAInfo indicates that the control plane
|
||||
// believes TKA should not be enabled. An initial non-nil TKAInfo
|
||||
// indicates the control plane believes TKA should be enabled.
|
||||
// A nil TKAInfo in a mapresponse stream (i.e. a 'delta' mapresponse)
|
||||
// indicates no change from the value sent earlier.
|
||||
TKAInfo *TKAInfo `json:",omitempty"`
|
||||
|
||||
// DomainDataPlaneAuditLogID, if non-empty, is the per-tailnet log ID to be
|
||||
// used when writing data plane audit logs.
|
||||
DomainDataPlaneAuditLogID string `json:",omitempty"`
|
||||
@@ -2154,7 +2034,6 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
n.UnsignedPeerAPIOnly == n2.UnsignedPeerAPIOnly &&
|
||||
n.Key == n2.Key &&
|
||||
n.KeyExpiry.Equal(n2.KeyExpiry) &&
|
||||
bytes.Equal(n.KeySignature, n2.KeySignature) &&
|
||||
n.Machine == n2.Machine &&
|
||||
n.DiscoKey == n2.DiscoKey &&
|
||||
eqPtr(n.Online, n2.Online) &&
|
||||
@@ -2524,232 +2403,6 @@ type SetDeviceAttributesRequest struct {
|
||||
// for each attribute value, like an expiry time?
|
||||
type AttrUpdate map[string]any
|
||||
|
||||
// SSHPolicy is the policy for how to handle incoming SSH connections
|
||||
// over Tailscale.
|
||||
type SSHPolicy struct {
|
||||
// Rules are the rules to process for an incoming SSH connection. The first
|
||||
// matching rule takes its action and stops processing further rules.
|
||||
//
|
||||
// When an incoming connection first starts, all rules are evaluated in
|
||||
// "none" auth mode, where the client hasn't even been asked to send a
|
||||
// public key. All SSHRule.Principals requiring a public key won't match. If
|
||||
// a rule matches on the first pass and its Action is reject, the
|
||||
// authentication fails with that action's rejection message, if any.
|
||||
//
|
||||
// If the first pass rule evaluation matches nothing without matching an
|
||||
// Action with Reject set, the rules are considered to see whether public
|
||||
// keys might still result in a match. If not, "none" auth is terminated
|
||||
// before proceeding to public key mode. If so, the client is asked to try
|
||||
// public key authentication and the rules are evaluated again for each of
|
||||
// the client's present keys.
|
||||
Rules []*SSHRule `json:"rules"`
|
||||
}
|
||||
|
||||
// An SSH rule is a match predicate and associated action for an incoming SSH connection.
|
||||
type SSHRule struct {
|
||||
// RuleExpires, if non-nil, is when this rule expires.
|
||||
//
|
||||
// For example, a (principal,sshuser) tuple might be granted
|
||||
// prompt-free SSH access for N minutes, so this rule would be
|
||||
// before a expiration-free rule for the same principal that
|
||||
// required an auth prompt. This permits the control plane to
|
||||
// be out of the path for already-authorized SSH pairs.
|
||||
//
|
||||
// Once a rule matches, the lifetime of any accepting connection
|
||||
// is subject to the SSHAction.SessionExpires time, if any.
|
||||
RuleExpires *time.Time `json:"ruleExpires,omitempty"`
|
||||
|
||||
// Principals matches an incoming connection. If the connection
|
||||
// matches anything in this list and also matches SSHUsers,
|
||||
// then Action is applied.
|
||||
Principals []*SSHPrincipal `json:"principals"`
|
||||
|
||||
// SSHUsers are the SSH users that this rule matches. It is a
|
||||
// map from either ssh-user|"*" => local-user. The map must
|
||||
// contain a key for either ssh-user or, as a fallback, "*" to
|
||||
// match anything. If it does, the map entry's value is the
|
||||
// actual user that's logged in.
|
||||
// If the map value is the empty string (for either the
|
||||
// requested SSH user or "*"), the rule doesn't match.
|
||||
// If the map value is "=", it means the ssh-user should map
|
||||
// directly to the local-user.
|
||||
// It may be nil if the Action is reject.
|
||||
SSHUsers map[string]string `json:"sshUsers"`
|
||||
|
||||
// Action is the outcome to task.
|
||||
// A nil or invalid action means to deny.
|
||||
Action *SSHAction `json:"action"`
|
||||
|
||||
// AcceptEnv is a slice of environment variable names that are allowlisted
|
||||
// for the SSH rule in the policy file.
|
||||
//
|
||||
// AcceptEnv values may contain * and ? wildcard characters which match against
|
||||
// an arbitrary number of characters or a single character respectively.
|
||||
AcceptEnv []string `json:"acceptEnv,omitempty"`
|
||||
}
|
||||
|
||||
// SSHPrincipal is either a particular node or a user on any node.
|
||||
type SSHPrincipal struct {
|
||||
// Matching any one of the following four field causes a match.
|
||||
// It must also match Certs, if non-empty.
|
||||
|
||||
Node StableNodeID `json:"node,omitempty"`
|
||||
NodeIP string `json:"nodeIP,omitempty"`
|
||||
UserLogin string `json:"userLogin,omitempty"` // email-ish: foo@example.com, bar@github
|
||||
Any bool `json:"any,omitempty"` // if true, match any connection
|
||||
// TODO(bradfitz): add StableUserID, once that exists
|
||||
|
||||
// UnusedPubKeys was public key support. It never became an official product
|
||||
// feature and so as of 2024-12-12 is being removed.
|
||||
// This stub exists to remind us not to re-use the JSON field name "pubKeys"
|
||||
// in the future if we bring it back with different semantics.
|
||||
//
|
||||
// Deprecated: do not use. It does nothing.
|
||||
UnusedPubKeys []string `json:"pubKeys,omitempty"`
|
||||
}
|
||||
|
||||
// SSHAction is how to handle an incoming connection.
|
||||
// At most one field should be non-zero.
|
||||
type SSHAction struct {
|
||||
// Message, if non-empty, is shown to the user before the
|
||||
// action occurs.
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Reject, if true, terminates the connection. This action
|
||||
// has higher priority that Accept, if given.
|
||||
// The reason this is exists is primarily so a response
|
||||
// from HoldAndDelegate has a way to stop the poll.
|
||||
Reject bool `json:"reject,omitempty"`
|
||||
|
||||
// Accept, if true, accepts the connection immediately
|
||||
// without further prompts.
|
||||
Accept bool `json:"accept,omitempty"`
|
||||
|
||||
// SessionDuration, if non-zero, is how long the session can stay open
|
||||
// before being forcefully terminated.
|
||||
SessionDuration time.Duration `json:"sessionDuration,omitempty"`
|
||||
|
||||
// AllowAgentForwarding, if true, allows accepted connections to forward
|
||||
// the ssh agent if requested.
|
||||
AllowAgentForwarding bool `json:"allowAgentForwarding,omitempty"`
|
||||
|
||||
// HoldAndDelegate, if non-empty, is a URL that serves an
|
||||
// outcome verdict. The connection will be accepted and will
|
||||
// block until the provided long-polling URL serves a new
|
||||
// SSHAction JSON value. The URL must be fetched using the
|
||||
// Noise transport (in package control/control{base,http}).
|
||||
// If the long poll breaks before returning a complete HTTP
|
||||
// response, it should be re-fetched as long as the SSH
|
||||
// session is open.
|
||||
//
|
||||
// The following variables in the URL are expanded by tailscaled:
|
||||
//
|
||||
// * $SRC_NODE_IP (URL escaped)
|
||||
// * $SRC_NODE_ID (Node.ID as int64 string)
|
||||
// * $DST_NODE_IP (URL escaped)
|
||||
// * $DST_NODE_ID (Node.ID as int64 string)
|
||||
// * $SSH_USER (URL escaped, ssh user requested)
|
||||
// * $LOCAL_USER (URL escaped, local user mapped)
|
||||
HoldAndDelegate string `json:"holdAndDelegate,omitempty"`
|
||||
|
||||
// AllowLocalPortForwarding, if true, allows accepted connections
|
||||
// to use local port forwarding if requested.
|
||||
AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"`
|
||||
|
||||
// AllowRemotePortForwarding, if true, allows accepted connections
|
||||
// to use remote port forwarding if requested.
|
||||
AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"`
|
||||
|
||||
// Recorders defines the destinations of the SSH session recorders.
|
||||
// The recording will be uploaded to http://addr:port/record.
|
||||
Recorders []netip.AddrPort `json:"recorders,omitempty"`
|
||||
|
||||
// OnRecorderFailure is the action to take if recording fails.
|
||||
// If nil, the default action is to fail open.
|
||||
OnRecordingFailure *SSHRecorderFailureAction `json:"onRecordingFailure,omitempty"`
|
||||
}
|
||||
|
||||
// SSHRecorderFailureAction is the action to take if recording fails.
|
||||
type SSHRecorderFailureAction struct {
|
||||
// RejectSessionWithMessage, if not empty, specifies that the session should
|
||||
// be rejected if the recording fails to start.
|
||||
// The message will be shown to the user before the session is rejected.
|
||||
RejectSessionWithMessage string `json:",omitempty"`
|
||||
|
||||
// TerminateSessionWithMessage, if not empty, specifies that the session
|
||||
// should be terminated if the recording fails after it has started. The
|
||||
// message will be shown to the user before the session is terminated.
|
||||
TerminateSessionWithMessage string `json:",omitempty"`
|
||||
|
||||
// NotifyURL, if non-empty, specifies a HTTP POST URL to notify when the
|
||||
// recording fails. The payload is the JSON encoded
|
||||
// SSHRecordingFailureNotifyRequest struct. The host field in the URL is
|
||||
// ignored, and it will be sent to control over the Noise transport.
|
||||
NotifyURL string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// SSHEventNotifyRequest is the JSON payload sent to the NotifyURL
|
||||
// for an SSH event.
|
||||
type SSHEventNotifyRequest struct {
|
||||
// EventType is the type of notify request being sent.
|
||||
EventType SSHEventType
|
||||
|
||||
// ConnectionID uniquely identifies a connection made to the SSH server.
|
||||
// It may be shared across multiple sessions over the same connection in
|
||||
// case a single connection creates multiple sessions.
|
||||
ConnectionID string
|
||||
|
||||
// CapVersion is the client's current CapabilityVersion.
|
||||
CapVersion CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// SrcNode is the ID of the node that initiated the SSH session.
|
||||
SrcNode NodeID
|
||||
|
||||
// SSHUser is the user that was presented to the SSH server.
|
||||
SSHUser string
|
||||
|
||||
// LocalUser is the user that was resolved from the SSHUser for the local machine.
|
||||
LocalUser string
|
||||
|
||||
// RecordingAttempts is the list of recorders that were attempted, in order.
|
||||
RecordingAttempts []*SSHRecordingAttempt
|
||||
}
|
||||
|
||||
// SSHEventType defines the event type linked to a SSH action or state.
|
||||
type SSHEventType int
|
||||
|
||||
const (
|
||||
UnspecifiedSSHEventType SSHEventType = 0
|
||||
// SSHSessionRecordingRejected is the event that
|
||||
// defines when a SSH session cannot be started
|
||||
// because no recorder is available for session
|
||||
// recording, and the SSHRecorderFailureAction
|
||||
// RejectSessionWithMessage is not empty.
|
||||
SSHSessionRecordingRejected SSHEventType = 1
|
||||
// SSHSessionRecordingTerminated is the event that
|
||||
// defines when session recording has failed
|
||||
// during the session and the SSHRecorderFailureAction
|
||||
// TerminateSessionWithMessage is not empty.
|
||||
SSHSessionRecordingTerminated SSHEventType = 2
|
||||
// SSHSessionRecordingFailed is the event that
|
||||
// defines when session recording is unavailable and
|
||||
// the SSHRecorderFailureAction RejectSessionWithMessage
|
||||
// or TerminateSessionWithMessage is empty.
|
||||
SSHSessionRecordingFailed SSHEventType = 3
|
||||
)
|
||||
|
||||
// SSHRecordingAttempt is a single attempt to start a recording.
|
||||
type SSHRecordingAttempt struct {
|
||||
// Recorder is the address of the recorder that was attempted.
|
||||
Recorder netip.AddrPort
|
||||
|
||||
// FailureMessage is the error message of the failed attempt.
|
||||
FailureMessage string
|
||||
}
|
||||
|
||||
// QueryFeatureRequest is a request sent to "/machine/feature/query"
|
||||
// to get instructions on how to enable a feature, such as Funnel,
|
||||
// for the node's tailnet.
|
||||
@@ -2901,10 +2554,6 @@ type PeerChange struct {
|
||||
// Key, if non-nil, means that the NodeID's wireguard public key changed.
|
||||
Key *key.NodePublic `json:",omitempty"`
|
||||
|
||||
// KeySignature, if non-nil, means that the signature of the wireguard
|
||||
// public key has changed.
|
||||
KeySignature tkatype.MarshaledSignature `json:",omitempty"`
|
||||
|
||||
// DiscoKey, if non-nil, means that the NodeID's discokey changed.
|
||||
DiscoKey *key.DiscoPublic `json:",omitempty"`
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user