Compare commits

..

1 Commits
v1.32.3 ... vm

Author SHA1 Message Date
Denton Gentry
f63619299b Try running vm.yml on a 22.04 runner.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-09-27 20:11:43 -07:00
180 changed files with 1605 additions and 5297 deletions

View File

@@ -11,7 +11,7 @@ concurrency:
jobs:
ubuntu2004-LTS-cloud-base:
runs-on: [ self-hosted, linux, vm ]
runs-on: ubuntu-22.04
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -32,7 +32,7 @@ jobs:
env:
HOME: "/tmp"
TMPDIR: "/tmp"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
XDG_CACHE_HOME: "$HOME/.cache"
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -1 +1 @@
1.32.3
1.31.0

View File

@@ -106,10 +106,10 @@ func TestChirp(t *testing.T) {
t.Fatal(err)
}
if err := c.EnableProtocol("rando"); err == nil {
t.Fatalf("enabling %q succeeded", "rando")
t.Fatalf("enabling %q succeded", "rando")
}
if err := c.DisableProtocol("rando"); err == nil {
t.Fatalf("disabling %q succeeded", "rando")
t.Fatalf("disabling %q succeded", "rando")
}
}

View File

@@ -459,7 +459,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("control api responded with %d status code", resp.StatusCode)
return nil, fmt.Errorf("control api responsed with %d status code", resp.StatusCode)
}
// The test ran without fail

View File

@@ -7,9 +7,6 @@ package apitype
import "tailscale.com/tailcfg"
// LocalAPIHost is the Host header value used by the LocalAPI.
const LocalAPIHost = "local-tailscaled.sock"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *tailcfg.Node

View File

@@ -197,7 +197,7 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
}
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body)
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil {
return nil, err
}
@@ -276,12 +276,6 @@ type BugReportOpts struct {
// Diagnose specifies whether to print additional diagnostic information to
// the logs when generating this bugreport.
Diagnose bool
// Record specifies, if non-nil, whether to perform a bugreport
// "recording"generating an initial log marker, then waiting for
// this channel to be closed before finishing the request, which
// generates another log marker.
Record <-chan struct{}
}
// BugReportWithOpts logs and returns a log marker that can be shared by the
@@ -290,40 +284,16 @@ type BugReportOpts struct {
// The opts type specifies options to pass to the Tailscale daemon when
// generating this bug report.
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
qparams := make(url.Values)
var qparams url.Values
if opts.Note != "" {
qparams.Set("note", opts.Note)
}
if opts.Diagnose {
qparams.Set("diagnose", "true")
}
if opts.Record != nil {
qparams.Set("record", "true")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var requestBody io.Reader
if opts.Record != nil {
pr, pw := io.Pipe()
requestBody = pr
// This goroutine waits for the 'Record' channel to be closed,
// and then closes the write end of our pipe to unblock the
// reader.
go func() {
defer pw.Close()
select {
case <-opts.Record:
case <-ctx.Done():
}
}()
}
// lc.send might block if opts.Record != nil; see above.
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
body, err := lc.send(ctx, "POST", uri, 200, requestBody)
body, err := lc.send(ctx, "POST", uri, 200, nil)
if err != nil {
return "", err
}
@@ -348,28 +318,6 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil
}
// SetComponentDebugLogging sets component's debug logging enabled for
// the provided duration. If the duration is in the past, the debug logging
// is disabled.
func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
body, err := lc.send(ctx, "POST",
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
var res struct {
Error string
}
if err := json.Unmarshal(body, &res); err != nil {
return err
}
if res.Error != "" {
return errors.New(res.Error)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.Status(ctx)
@@ -435,7 +383,7 @@ func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) e
}
func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/files/"+url.PathEscape(baseName), nil)
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return nil, 0, err
}
@@ -472,7 +420,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
}
@@ -595,7 +543,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
},
}
ctx = httptrace.WithClientTrace(ctx, &trace)
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/dial", nil)
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
if err != nil {
return nil, err
}
@@ -726,14 +674,14 @@ func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate
return &cert, nil
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
//
// Deprecated: use LocalClient.ExpandSNIName.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultLocalClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
@@ -800,30 +748,6 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ip
return pr, nil
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) (*ipnstate.NetworkLockStatus, 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 nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 200, &b)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
pr := new(ipnstate.NetworkLockStatus)
if err := json.Unmarshal(body, pr); err != nil {
return nil, err
}
return pr, nil
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections.
//

View File

@@ -115,7 +115,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.httpClient().Do(req)
}
// sendRequest add the authentication key to the request and sends it. It
// sendRequest add the authenication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
if !I_Acknowledge_This_API_Is_Unstable {

View File

@@ -47,7 +47,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
tailscale.com/paths from tailscale.com/client/tailscale
tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
@@ -108,8 +107,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
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+

View File

@@ -325,31 +325,11 @@ func main() {
}
}
const (
noContentChallengeHeader = "X-Tailscale-Challenge"
noContentResponseHeader = "X-Tailscale-Response"
)
// For captive portal detection
func serveNoContent(w http.ResponseWriter, r *http.Request) {
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
badChar := strings.IndexFunc(challenge, func(r rune) bool {
return !isChallengeChar(r)
}) != -1
if len(challenge) <= 64 && !badChar {
w.Header().Set(noContentResponseHeader, "response "+challenge)
}
}
w.WriteHeader(http.StatusNoContent)
}
func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
}
// probeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func probeHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -7,9 +7,6 @@ package main
import (
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"tailscale.com/net/stun"
@@ -70,57 +67,3 @@ func BenchmarkServerSTUN(b *testing.B) {
}
}
func TestNoContent(t *testing.T) {
testCases := []struct {
name string
input string
want string
}{
{
name: "no challenge",
},
{
name: "valid challenge",
input: "input",
want: "response input",
},
{
name: "invalid challenge",
input: "foo\x00bar",
want: "",
},
{
name: "whitespace invalid challenge",
input: "foo bar",
want: "",
},
{
name: "long challenge",
input: strings.Repeat("x", 65),
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
if tt.input != "" {
req.Header.Set(noContentChallengeHeader, tt.input)
}
w := httptest.NewRecorder()
serveNoContent(w, req)
resp := w.Result()
if tt.want == "" {
if h, found := resp.Header[noContentResponseHeader]; found {
t.Errorf("got %+v; expected no response header", h)
}
return
}
if got := resp.Header.Get(noContentResponseHeader); got != tt.want {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}

View File

@@ -13,7 +13,6 @@ import (
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/net/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
@@ -24,7 +23,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
up := strings.ToLower(r.Header.Get("Upgrade"))
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
base.ServeHTTP(w, r)
@@ -51,7 +50,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
return
}
counterWebSocketAccepts.Add(1)
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary)
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
})

View File

@@ -1,42 +0,0 @@
# pgproxy
The pgproxy server is a proxy for the Postgres wire protocol. [Read
more in our blog
post](https://tailscale.com/blog/introducing-pgproxy/) about it!
The proxy runs an in-process Tailscale instance, accepts postgres
client connections over Tailscale only, and proxies them to the
configured upstream postgres server.
This proxy exists because postgres clients default to very insecure
connection settings: either they "prefer" but do not require TLS; or
they set sslmode=require, which merely requires that a TLS handshake
took place, but don't verify the server's TLS certificate or the
presented TLS hostname. In other words, sslmode=require enforces that
a TLS session is created, but that session can trivially be
machine-in-the-middled to steal credentials, data, inject malicious
queries, and so forth.
Because this flaw is in the client's validation of the TLS session,
you have no way of reliably detecting the misconfiguration
server-side. You could fix the configuration of all the clients you
know of, but the default makes it very easy to accidentally regress.
Instead of trying to verify client configuration over time, this proxy
removes the need for postgres clients to be configured correctly: the
upstream database is configured to only accept connections from the
proxy, and the proxy is only available to clients over Tailscale.
Therefore, clients must use the proxy to connect to the database. The
client<>proxy connection is secured end-to-end by Tailscale, which the
proxy enforces by verifying that the connecting client is a known
current Tailscale peer. The proxy<>server connection is established by
the proxy itself, using strict TLS verification settings, and the
client is only allowed to communicate with the server once we've
established that the upstream connection is safe to use.
A couple side benefits: because clients can only connect via
Tailscale, you can use Tailscale ACLs as an extra layer of defense on
top of the postgres user/password authentication. And, the proxy can
maintain an audit log of who connected to the database, complete with
the strongly authenticated Tailscale identity of the client.

View File

@@ -1,366 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The pgproxy server is a proxy for the Postgres wire protocol.
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"expvar"
"flag"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"os"
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/metrics"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
"tailscale.com/types/logger"
)
var (
hostname = flag.String("hostname", "", "Tailscale hostname to serve on")
port = flag.Int("port", 5432, "Listening port for client connections")
debugPort = flag.Int("debug-port", 80, "Listening port for debug/metrics endpoint")
upstreamAddr = flag.String("upstream-addr", "", "Address of the upstream Postgres server, in host:port format")
upstreamCA = flag.String("upstream-ca-file", "", "File containing the PEM-encoded CA certificate for the upstream server")
tailscaleDir = flag.String("state-dir", "", "Directory in which to store the Tailscale auth state")
)
func main() {
flag.Parse()
if *hostname == "" {
log.Fatal("missing --hostname")
}
if *upstreamAddr == "" {
log.Fatal("missing --upstream-addr")
}
if *upstreamCA == "" {
log.Fatal("missing --upstream-ca-file")
}
if *tailscaleDir == "" {
log.Fatal("missing --state-dir")
}
ts := &tsnet.Server{
Dir: *tailscaleDir,
Hostname: *hostname,
// Make the stdout logs a clean audit log of connections.
Logf: logger.Discard,
}
if os.Getenv("TS_AUTHKEY") == "" {
log.Print("Note: you need to run this with TS_AUTHKEY=... the first time, to join your tailnet of choice.")
}
tsclient, err := ts.LocalClient()
if err != nil {
log.Fatalf("getting tsnet API client: %v", err)
}
p, err := newProxy(*upstreamAddr, *upstreamCA, tsclient)
if err != nil {
log.Fatal(err)
}
expvar.Publish("pgproxy", p.Expvar())
if *debugPort != 0 {
mux := http.NewServeMux()
tsweb.Debugger(mux)
srv := &http.Server{
Handler: mux,
}
dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(srv.Serve(dln))
}()
}
ln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
log.Printf("serving access to %s on port %d", *upstreamAddr, *port)
log.Fatal(p.Serve(ln))
}
// proxy is a postgres wire protocol proxy, which strictly enforces
// the security of the TLS connection to its upstream regardless of
// what the client's TLS configuration is.
type proxy struct {
upstreamAddr string // "my.database.com:5432"
upstreamHost string // "my.database.com"
upstreamCertPool *x509.CertPool
downstreamCert []tls.Certificate
client *tailscale.LocalClient
activeSessions expvar.Int
startedSessions expvar.Int
errors metrics.LabelMap
}
// newProxy returns a proxy that forwards connections to
// upstreamAddr. The upstream's TLS session is verified using the CA
// cert(s) in upstreamCAPath.
func newProxy(upstreamAddr, upstreamCAPath string, client *tailscale.LocalClient) (*proxy, error) {
bs, err := os.ReadFile(upstreamCAPath)
if err != nil {
return nil, err
}
upstreamCertPool := x509.NewCertPool()
if !upstreamCertPool.AppendCertsFromPEM(bs) {
return nil, fmt.Errorf("invalid CA cert in %q", upstreamCAPath)
}
h, _, err := net.SplitHostPort(upstreamAddr)
if err != nil {
return nil, err
}
downstreamCert, err := mkSelfSigned(h)
if err != nil {
return nil, err
}
return &proxy{
upstreamAddr: upstreamAddr,
upstreamHost: h,
upstreamCertPool: upstreamCertPool,
downstreamCert: []tls.Certificate{downstreamCert},
client: client,
errors: metrics.LabelMap{Label: "kind"},
}, nil
}
// Expvar returns p's monitoring metrics.
func (p *proxy) Expvar() expvar.Var {
ret := &metrics.Set{}
ret.Set("sessions_active", &p.activeSessions)
ret.Set("sessions_started", &p.startedSessions)
ret.Set("session_errors", &p.errors)
return ret
}
// Serve accepts postgres client connections on ln and proxies them to
// the configured upstream. ln can be any net.Listener, but all client
// connections must originate from tailscale IPs that can be verified
// with WhoIs.
func (p *proxy) Serve(ln net.Listener) error {
var lastSessionID int64
for {
c, err := ln.Accept()
if err != nil {
return err
}
id := time.Now().UnixNano()
if id == lastSessionID {
// Bluntly enforce SID uniqueness, even if collisions are
// fantastically unlikely (but OSes vary in how much timer
// precision they expose to the OS, so id might be rounded
// e.g. to the same millisecond)
id++
}
lastSessionID = id
go func(sessionID int64) {
if err := p.serve(sessionID, c); err != nil {
log.Printf("%d: session ended with error: %v", sessionID, err)
}
}(id)
}
}
var (
// sslStart is the magic bytes that postgres clients use to indicate
// that they want to do a TLS handshake. Servers should respond with
// the single byte "S" before starting a normal TLS handshake.
sslStart = [8]byte{0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f}
// plaintextStart is the magic bytes that postgres clients use to
// indicate that they're starting a plaintext authentication
// handshake.
plaintextStart = [8]byte{0, 0, 0, 86, 0, 3, 0, 0}
)
// serve proxies the postgres client on c to the proxy's upstream,
// enforcing strict TLS to the upstream.
func (p *proxy) serve(sessionID int64, c net.Conn) error {
defer c.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
whois, err := p.client.WhoIs(ctx, c.RemoteAddr().String())
if err != nil {
p.errors.Add("whois-failed", 1)
return fmt.Errorf("getting client identity: %v", err)
}
// Before anything else, log the connection attempt.
user, machine := "", ""
if whois.Node != nil {
if whois.Node.Hostinfo.ShareeNode() {
machine = "external-device"
} else {
machine = strings.TrimSuffix(whois.Node.Name, ".")
}
}
if whois.UserProfile != nil {
user = whois.UserProfile.LoginName
if user == "tagged-devices" && whois.Node != nil {
user = strings.Join(whois.Node.Tags, ",")
}
}
if user == "" || machine == "" {
p.errors.Add("no-ts-identity", 1)
return fmt.Errorf("couldn't identify source user and machine (user %q, machine %q)", user, machine)
}
log.Printf("%d: session start, from %s (machine %s, user %s)", sessionID, c.RemoteAddr(), machine, user)
start := time.Now()
defer func() {
elapsed := time.Since(start)
log.Printf("%d: session end, from %s (machine %s, user %s), lasted %s", sessionID, c.RemoteAddr(), machine, user, elapsed.Round(time.Millisecond))
}()
// Read the client's opening message, to figure out if it's trying
// to TLS or not.
var buf [8]byte
if _, err := io.ReadFull(c, buf[:len(sslStart)]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("initial magic read: %v", err)
}
var clientIsTLS bool
switch {
case buf == sslStart:
clientIsTLS = true
case buf == plaintextStart:
clientIsTLS = false
default:
p.errors.Add("client-bad-protocol", 1)
return fmt.Errorf("unrecognized initial packet = % 02x", buf)
}
// Dial & verify upstream connection.
var d net.Dialer
d.Timeout = 10 * time.Second
upc, err := d.Dial("tcp", p.upstreamAddr)
if err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("upstream dial: %v", err)
}
defer upc.Close()
if _, err := upc.Write(sslStart[:]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("upstream write of start-ssl magic: %v", err)
}
if _, err := io.ReadFull(upc, buf[:1]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("reading upstream start-ssl response: %v", err)
}
if buf[0] != 'S' {
p.errors.Add("upstream-bad-protocol", 1)
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
}
tlsConf := &tls.Config{
ServerName: p.upstreamHost,
RootCAs: p.upstreamCertPool,
MinVersion: tls.VersionTLS12,
}
uptc := tls.Client(upc, tlsConf)
if err = uptc.HandshakeContext(ctx); err != nil {
p.errors.Add("upstream-tls", 1)
return fmt.Errorf("upstream TLS handshake: %v", err)
}
// Accept the client conn and set it up the way the client wants.
var clientConn net.Conn
if clientIsTLS {
io.WriteString(c, "S") // yeah, we're good to speak TLS
s := tls.Server(c, &tls.Config{
ServerName: p.upstreamHost,
Certificates: p.downstreamCert,
MinVersion: tls.VersionTLS12,
})
if err = uptc.HandshakeContext(ctx); err != nil {
p.errors.Add("client-tls", 1)
return fmt.Errorf("client TLS handshake: %v", err)
}
clientConn = s
} else {
// Repeat the header we read earlier up to the server.
if _, err := uptc.Write(plaintextStart[:]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("sending initial client bytes to upstream: %v", err)
}
clientConn = c
}
// Finally, proxy the client to the upstream.
errc := make(chan error, 1)
go func() {
_, err := io.Copy(uptc, clientConn)
errc <- err
}()
go func() {
_, err := io.Copy(clientConn, uptc)
errc <- err
}()
if err := <-errc; err != nil {
// Don't increment error counts here, because the most common
// cause of termination is client or server closing the
// connection normally, and it'll obscure "interesting"
// handshake errors.
return fmt.Errorf("session terminated with error: %v", err)
}
return nil
}
// mkSelfSigned creates and returns a self-signed TLS certificate for
// hostname.
func mkSelfSigned(hostname string) (tls.Certificate, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
if err != nil {
return tls.Certificate{}, err
}
pub := priv.Public()
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"pgproxy"},
},
DNSNames: []string{hostname},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(crand.Reader, &template, &template, pub, priv)
if err != nil {
return tls.Certificate{}, err
}
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{derBytes},
PrivateKey: priv,
Leaf: cert,
}, nil
}

View File

@@ -1,171 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// ssh-auth-none-demo is a demo SSH server that's meant to run on the
// public internet and highlight the unique parts of the Tailscale SSH
// server so SSH client authors can hit it easily and fix their SSH
// clients without needing to set up Tailscale and Tailscale SSH.
package main
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
)
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
var (
addr = flag.String("addr", ":2222", "address to listen on")
)
func main() {
flag.Parse()
cacheDir, err := os.UserCacheDir()
if err != nil {
log.Fatal(err)
}
dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Fatal(err)
}
keys, err := getHostKeys(dir)
if err != nil {
log.Fatal(err)
}
if len(keys) == 0 {
log.Fatal("no host keys")
}
srv := &ssh.Server{
Addr: *addr,
Version: "Tailscale",
Handler: handleSessionPostSSHAuth,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
ImplicitAuthMethod: "tailscale",
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(gossh.ConnMetadata) (*gossh.Permissions, error) {
return nil, nil
},
BannerCallback: func(cm gossh.ConnMetadata) string {
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
},
}
},
}
for _, signer := range keys {
srv.AddHostKey(signer)
}
log.Printf("Running on %s ...", srv.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
log.Printf("done")
}
func handleSessionPostSSHAuth(s ssh.Session) {
log.Printf("Started session from user %q", s.User())
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
// Abort the session on Control-C or Control-D.
go func() {
buf := make([]byte, 1024)
for {
n, err := s.Read(buf)
for _, b := range buf[:n] {
if b <= 4 { // abort on Control-C (3) or Control-D (4)
io.WriteString(s, "bye\n")
s.Exit(1)
}
}
if err != nil {
return
}
}
}()
for i := 10; i > 0; i-- {
fmt.Fprintf(s, "%v ...\n", i)
time.Sleep(time.Second)
}
s.Exit(0)
}
func getHostKeys(dir string) (ret []ssh.Signer, err error) {
for _, typ := range keyTypes {
hostKey, err := hostKeyFileOrCreate(dir, typ)
if err != nil {
return nil, err
}
signer, err := gossh.ParsePrivateKey(hostKey)
if err != nil {
return nil, err
}
ret = append(ret, signer)
}
return ret, nil
}
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := ioutil.ReadFile(path)
if err == nil {
return v, nil
}
if !os.IsNotExist(err) {
return nil, err
}
var priv any
switch typ {
default:
return nil, fmt.Errorf("unsupported key type %q", typ)
case "ed25519":
_, priv, err = ed25519.GenerateKey(rand.Reader)
case "ecdsa":
// curve is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
curve := elliptic.P256()
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
case "rsa":
// keySize is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
const keySize = 2048
priv, err = rsa.GenerateKey(rand.Reader, keySize)
}
if err != nil {
return nil, err
}
mk, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
err = os.WriteFile(path, pemGen, 0700)
return pemGen, err
}

View File

@@ -8,7 +8,6 @@ import (
"context"
"errors"
"flag"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
@@ -22,14 +21,12 @@ var bugReportCmd = &ffcli.Command{
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
fs.BoolVar(&bugReportArgs.record, "record", false, "if true, pause and then write another bugreport")
return fs
})(),
}
var bugReportArgs struct {
diagnose bool
record bool
}
func runBugReport(ctx context.Context, args []string) error {
@@ -39,46 +36,15 @@ func runBugReport(ctx context.Context, args []string) error {
case 1:
note = args[0]
default:
return errors.New("unknown arguments")
return errors.New("unknown argumets")
}
opts := tailscale.BugReportOpts{
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{
Note: note,
Diagnose: bugReportArgs.diagnose,
})
if err != nil {
return err
}
if !bugReportArgs.record {
// Simple, non-record case
logMarker, err := localClient.BugReportWithOpts(ctx, opts)
if err != nil {
return err
}
outln(logMarker)
return nil
}
// Recording; run the request in the background
done := make(chan struct{})
opts.Record = done
type bugReportResp struct {
marker string
err error
}
resCh := make(chan bugReportResp, 1)
go func() {
m, err := localClient.BugReportWithOpts(ctx, opts)
resCh <- bugReportResp{m, err}
}()
outln("Recording started; please reproduce your issue and then press Enter...")
fmt.Scanln()
close(done)
res := <-resCh
if res.err != nil {
return res.err
}
outln(res.marker)
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
outln(logMarker)
return nil
}

View File

@@ -410,7 +410,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Issue 3480
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
curPrefs: &ipn.Prefs{
@@ -448,7 +448,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// might've had an old install, and we don't support --accept-routes anyway.
// migth've had old an install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{

View File

@@ -42,7 +42,7 @@ var debugCmd = &ffcli.Command{
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-seconds seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
return fs
@@ -53,16 +53,6 @@ var debugCmd = &ffcli.Command{
Exec: runDERPMap,
ShortHelp: "print DERP map",
},
{
Name: "component-logs",
Exec: runDebugComponentLogs,
ShortHelp: "enable/disable debug logs for a component",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
return fs
})(),
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
@@ -523,26 +513,3 @@ func runTS2021(ctx context.Context, args []string) error {
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
return nil
}
var debugComponentLogsArgs struct {
forDur time.Duration
}
func runDebugComponentLogs(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: debug component-logs <component>")
}
component := args[0]
dur := debugComponentLogsArgs.forDur
err := localClient.SetComponentDebugLogging(ctx, component, dur)
if err != nil {
return err
}
if debugComponentLogsArgs.forDur <= 0 {
fmt.Printf("Disabled debug logs for component %q\n", component)
} else {
fmt.Printf("Enabled debug logs for component %q for %v\n", component, dur)
}
return nil
}

View File

@@ -29,7 +29,6 @@ import (
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/util/quarantine"
"tailscale.com/version"
)
@@ -394,10 +393,6 @@ func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targe
if err != nil {
return "", 0, err
}
// Apply quarantine attribute before copying
if err := quarantine.SetOnFile(f); err != nil {
return "", 0, fmt.Errorf("failed to apply quarantine attribute to file %v: %v", f.Name(), err)
}
_, err = io.Copy(f, rc)
if err != nil {
f.Close()

View File

@@ -17,16 +17,11 @@ import (
)
var netlockCmd = &ffcli.Command{
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortHelp: "Manipulate the tailnet key authority",
Subcommands: []*ffcli.Command{
nlInitCmd,
nlStatusCmd,
nlAddCmd,
nlRemoveCmd,
},
Exec: runNetworkLockStatus,
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortHelp: "Manipulate the tailnet key authority",
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd},
Exec: runNetworkLockStatus,
}
var nlInitCmd = &ffcli.Command{
@@ -46,9 +41,29 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
}
// Parse the set of initially-trusted keys.
keys, err := parseNLKeyArgs(args)
if err != nil {
return err
// Keys are specified using their key.NLPublic.MarshalText representation,
// with an optional '?<votes>' suffix.
var keys []tka.Key
for i, a := range args {
var key key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := key.UnmarshalText([]byte(spl[0])); err != nil {
return fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
Kind: tka.Key25519,
Public: key.Verifier(),
Votes: 1,
}
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
}
status, err := localClient.NetworkLockInit(ctx, keys)
@@ -84,78 +99,3 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
fmt.Printf("our public-key: %s\n", p)
return nil
}
var nlAddCmd = &ffcli.Command{
Name: "add",
ShortUsage: "add <public-key>...",
ShortHelp: "Adds one or more signing keys to the tailnet key authority",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, args, nil)
},
}
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove <public-key>...",
ShortHelp: "Removes one or more signing keys to the tailnet key authority",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, nil, args)
},
}
// parseNLKeyArgs converts a slice of strings into a slice of tka.Key. The keys
// should be specified using their key.NLPublic.MarshalText representation with
// an optional '?<votes>' suffix. If any of the keys encounters an error, a nil
// slice is returned along with an appropriate error.
func parseNLKeyArgs(args []string) ([]tka.Key, error) {
var keys []tka.Key
for i, a := range args {
var nlpk key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
return nil, fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
Kind: tka.Key25519,
Public: nlpk.Verifier(),
Votes: 1,
}
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
}
return keys, nil
}
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
if st.Enabled {
return errors.New("network-lock is already enabled")
}
addKeys, err := parseNLKeyArgs(addArgs)
if err != nil {
return err
}
removeKeys, err := parseNLKeyArgs(removeArgs)
if err != nil {
return err
}
status, err := localClient.NetworkLockModify(ctx, addKeys, removeKeys)
if err != nil {
return err
}
fmt.Printf("Status: %+v\n\n", status)
return nil
}

View File

@@ -13,7 +13,7 @@ import (
func findSSH() (string, error) {
// use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
// occurred with ssh.exe provided by msys2/cygwin and other environments.
// occured with ssh.exe provided by msys2/cygwin and other environments.
if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
if st, err := os.Stat(exe); err == nil && !st.IsDir() {

View File

@@ -501,7 +501,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
fatalf("%s", err)
}
if justEditMP != nil {
justEditMP.EggSet = egg
justEditMP.EggSet = true
_, err := localClient.EditPrefs(ctx, justEditMP)
return err
}

View File

@@ -474,8 +474,8 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
authURL = *url
cancel()
}
if !forceReauth && n.Prefs != nil && n.Prefs.Valid() {
p1, p2 := n.Prefs.AsStruct(), *prefs
if !forceReauth && n.Prefs != nil {
p1, p2 := *n.Prefs, *prefs
p1.Persist = nil
p2.Persist = nil
if p1.Equals(&p2) {

View File

@@ -7,7 +7,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
D github.com/google/uuid from tailscale.com/util/quarantine
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
@@ -71,7 +70,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/netcheck+
@@ -103,7 +101,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/lineread from tailscale.com/net/interfaces+
tailscale.com/util/mak from tailscale.com/net/netcheck
tailscale.com/util/multierr from tailscale.com/control/controlhttp
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
tailscale.com/util/singleflight from tailscale.com/net/dnscache
L tailscale.com/util/strs from tailscale.com/hostinfo
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
@@ -138,8 +135,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
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+
@@ -172,7 +167,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
D database/sql/driver from github.com/google/uuid
embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+
encoding/asn1 from crypto/x509+

View File

@@ -240,8 +240,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/net/dns+
tailscale.com/net/tunstats from tailscale.com/net/tstun
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+
@@ -323,7 +321,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -345,7 +342,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
golang.org/x/term from tailscale.com/logpolicy
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+

View File

@@ -88,7 +88,7 @@ func defaultTunName() string {
// see https://github.com/tailscale/tailscale/issues/391
//
// But Gokrazy does have the tun module built-in, so users
// can still run --tun=tailscale0 if they wish, if they
// can stil run --tun=tailscale0 if they wish, if they
// arrange for iptables to be present or run in "tailscale
// up --netfilter-mode=off" mode, perhaps. Untested.
return "userspace-networking"
@@ -158,7 +158,7 @@ func main() {
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`)
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
@@ -375,7 +375,8 @@ func run() error {
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
dialer := new(tsdial.Dialer) // mutated below (before used)
dialer.Logf = logf
e, useNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
return fmt.Errorf("createEngine: %w", err)

View File

@@ -23,7 +23,6 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/netip"
@@ -193,7 +192,7 @@ func beWindowsSubprocess() bool {
}
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file loggers add it.
// Remove the date/time prefix; the logtail + file logggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
@@ -266,17 +265,11 @@ func startIPNServer(ctx context.Context, logid string) error {
if err != nil {
return fmt.Errorf("monitor: %w", err)
}
dialer := &tsdial.Dialer{Logf: logf}
dialer := new(tsdial.Dialer)
getEngineRaw := func() (wgengine.Engine, *netstack.Impl, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) {
// Wintun is not installing correctly. Dump the state of NetSetupSvc
// (which is a user-mode service that must be active for network devices
// to install) and its dependencies to the log.
winutil.LogSvcState(logf, "NetSetupSvc")
}
return nil, nil, fmt.Errorf("TUN: %w", err)
}
r, err := router.New(logf, dev, nil)

View File

@@ -57,7 +57,7 @@ func runBuild() {
// fixEsbuildMetadataPaths re-keys the esbuild metadata file to use paths
// relative to the dist directory (it normally uses paths relative to the cwd,
// which are awkward if we're running with a different cwd at serving time).
// which are akward if we're running with a different cwd at serving time).
func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
var metadata EsbuildMetadata
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {

View File

@@ -10,9 +10,9 @@
"qrcode": "^1.5.0",
"tailwindcss": "^3.1.6",
"typescript": "^4.7.4",
"xterm": "^5.0.0",
"xterm-addon-fit": "^0.6.0",
"xterm-addon-web-links": "^0.7.0"
"xterm": "5.0.0-beta.58",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-web-links": "0.7.0-beta.6"
},
"scripts": {
"lint": "tsc --noEmit",

View File

@@ -42,7 +42,7 @@ export function runSSHSession(
term.focus()
let resizeObserver: ResizeObserver | undefined
let handleUnload: ((e: Event) => void) | undefined
let handleBeforeUnload: ((e: BeforeUnloadEvent) => void) | undefined
const sshSession = ipn.ssh(def.hostname, def.username, {
writeFn(input) {
@@ -60,8 +60,8 @@ export function runSSHSession(
onDone() {
resizeObserver?.disconnect()
term.dispose()
if (handleUnload) {
parentWindow.removeEventListener("unload", handleUnload)
if (handleBeforeUnload) {
parentWindow.removeEventListener("beforeunload", handleBeforeUnload)
}
onDone()
},
@@ -75,6 +75,6 @@ export function runSSHSession(
// Close the session if the user closes the window without an explicit
// exit.
handleUnload = () => sshSession.close()
parentWindow.addEventListener("unload", handleUnload)
handleBeforeUnload = () => sshSession.close()
parentWindow.addEventListener("beforeunload", handleBeforeUnload)
}

View File

@@ -15,12 +15,12 @@ import wasmURL from "./main.wasm"
* needed for the package to function.
*/
type IPNPackageConfig = IPNConfig & {
// Auth key used to initialize the Tailscale client (required)
// Auth key used to intitialize the Tailscale client (required)
authKey: string
// URL of the main.wasm file that is included in the page, if it is not
// accessible via a relative URL.
wasmURL?: string
// Function invoked if the Go process panics or unexpectedly exits.
// Funtion invoked if the Go process panics or unexpectedly exits.
panicHandler: (err: string) => void
}

View File

@@ -96,7 +96,7 @@ func newIPN(jsConfig js.Value) map[string]any {
logtail := logtail.NewLogger(c, log.Printf)
logf := logtail.Logf
dialer := &tsdial.Dialer{Logf: logf}
dialer := new(tsdial.Dialer)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Dialer: dialer,
})

View File

@@ -639,20 +639,20 @@ xtend@^4.0.2:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm-addon-fit@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.6.0.tgz#142e1ce181da48763668332593fc440349c88c34"
integrity sha512-9/7A+1KEjkFam0yxTaHfuk9LEvvTSBi0PZmEkzJqgafXPEXL9pCMAVV7rB09sX6ATRDXAdBpQhZkhKj7CGvYeg==
xterm-addon-fit@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
xterm-addon-web-links@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0.tgz#dceac36170605f9db10a01d716bd83ee38f65c17"
integrity sha512-6PqoqzzPwaeSq22skzbvyboDvSnYk5teUYEoKBwMYvhbkwOQkemZccjWHT5FnNA8o1aInTc4PRYAl4jjPucCKA==
xterm@5.0.0-beta.58:
version "5.0.0-beta.58"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.58.tgz#e3e96ab9fd24d006ec16cc9351a060cc79e67e80"
integrity sha512-gjg39oKdgUKful27+7I1hvSK51lu/LRhdimFhfZyMvdk0iATH0FAfzv1eAvBKWY2UBgYUfxhicTkanYioANdMw==
xterm@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c"
integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA==
xterm-addon-web-links@0.7.0-beta.6:
version "0.7.0-beta.6"
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0-beta.6.tgz#ec63b681b4f0f0135fa039f53664f65fe9d9f43a"
integrity sha512-nD/r/GchGTN4c9gAIVLWVoxExTzAUV7E9xZnwsvhuwI4CEE6yqO15ns8g2hdcUrsPyCbNEw05mIrkF6W5Yj8qA==
y18n@^4.0.0:
version "4.0.3"

View File

@@ -388,7 +388,7 @@ func main() {
log.Fatal(err)
}
if runCloner {
// When a new package is added or when existing generated files have
// When a new pacakge is added or when existing generated files have
// been deleted, we might run into a case where tailscale.com/cmd/cloner
// has not run yet. We detect this by verifying that all the structs we
// interacted with have had Clone method already generated. If they

View File

@@ -761,7 +761,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
request := &tailcfg.MapRequest{
Version: tailcfg.CurrentCapabilityVersion,
KeepAlive: c.keepAlive,
NodeKey: persist.PublicNodeKey(),
NodeKey: persist.PrivateNodeKey.Public(),
DiscoKey: c.discoPubKey,
Endpoints: epStrs,
EndpointTypes: epTypes,
@@ -776,7 +776,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyper-specific about the
// because the e2e tests are currently hyperspecific about the
// ordering of things. The e2e tests need love.
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
}

View File

@@ -35,7 +35,7 @@ type mapSession struct {
machinePubKey key.MachinePublic
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
// Fields storing state over the course of multiple MapResponses.
// Fields storing state over the the coards of multiple MapResponses.
lastNode *tailcfg.Node
lastDNSConfig *tailcfg.DNSConfig
lastDERPMap *tailcfg.DERPMap
@@ -45,7 +45,6 @@ type mapSession struct {
collectServices bool
previousPeers []*tailcfg.Node // for delta-purposes
lastDomain string
lastDomainAuditLogID string
lastHealth []string
lastPopBrowserURL string
stickyDebug tailcfg.Debug // accumulated opt.Bool values
@@ -114,9 +113,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
if resp.Domain != "" {
ms.lastDomain = resp.Domain
}
if resp.DomainDataPlaneAuditLogID != "" {
ms.lastDomainAuditLogID = resp.DomainDataPlaneAuditLogID
}
if resp.Health != nil {
ms.lastHealth = resp.Health
}
@@ -147,21 +143,20 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
nm := &netmap.NetworkMap{
NodeKey: ms.privateNodeKey.Public(),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
SSHPolicy: ms.lastSSHPolicy,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
Debug: debug,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
NodeKey: ms.privateNodeKey.Public(),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
SSHPolicy: ms.lastSSHPolicy,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
Debug: debug,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
}
ms.netMapBuilding = nm

View File

@@ -466,7 +466,7 @@ func TestNetmapForResponse(t *testing.T) {
})
}
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResponses
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResposnes
// entirely or have their opt.Bool values unspecified between MapResponses in a
// session and that should mean no change. (as of capver 37). But two Debug
// fields existed prior to capver 37 that weren't opt.Bool; we test that we both

View File

@@ -13,7 +13,6 @@ import (
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/wsconn"
)
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
@@ -52,7 +51,7 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if err != nil {
return nil, err
}
netConn := wsconn.NetConn(context.Background(), wsConn, websocket.MessageBinary)
netConn := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
cbConn, err := cont(ctx, netConn)
if err != nil {
netConn.Close()

View File

@@ -459,26 +459,13 @@ func TestDialPlan(t *testing.T) {
const (
testProtocolVersion = 1
// We need consistent ports for each address; these are chosen
// randomly and we hope that they won't conflict during this test.
httpPort = "40080"
httpsPort = "40443"
)
getRandomPort := func() string {
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("net.Listen: %v", err)
}
defer ln.Close()
_, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
t.Fatal(err)
}
return port
}
// We need consistent ports for each address; these are chosen
// randomly and we hope that they won't conflict during this test.
httpPort := getRandomPort()
httpsPort := getRandomPort()
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
done := make(chan struct{})
t.Cleanup(func() {

View File

@@ -14,7 +14,6 @@ import (
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
)
@@ -112,7 +111,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
}
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary)
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
conn.Close()

View File

@@ -232,7 +232,7 @@ func TestSendFreeze(t *testing.T) {
// alice --> bob
// alice --> cathy
//
// Then cathy stops processing messages.
// Then cathy stops processing messsages.
// That should not interfere with alice talking to bob.
newClient := func(ctx context.Context, name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
@@ -772,7 +772,7 @@ func TestForwarderRegistration(t *testing.T) {
})
// Now pretend u1 was already connected locally (so clientsMesh[u1] is nil), and then we heard
// that they're also connected to a peer of ours. That shouldn't transition the forwarder
// that they're also connected to a peer of ours. That sholdn't transition the forwarder
// from nil to the new one, not a multiForwarder.
s.clients[u1] = singleClient{u1c}
s.clientsMesh[u1] = nil

View File

@@ -96,7 +96,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion fun
return c
}
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
// NewNetcheckClient returns a Client that's only able to have its DialRegion method called.
// It's used by the netcheck package.
func NewNetcheckClient(logf logger.Logf) *Client {
return &Client{logf: logf}
@@ -199,7 +199,7 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
return fmt.Sprintf("https://%s/derp", node.HostName)
}
// AddressFamilySelector decides whether IPv6 is preferred for
// AddressFamilySelector decides whethers IPv6 is preferred for
// outbound dials.
type AddressFamilySelector interface {
// PreferIPv6 reports whether IPv4 dials should be slightly
@@ -985,9 +985,7 @@ func (c *Client) isClosed() bool {
// Close closes the client. It will not automatically reconnect after
// being closed.
func (c *Client) Close() error {
if c.cancelCtx != nil {
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
}
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
c.mu.Lock()
defer c.mu.Unlock()

View File

@@ -13,7 +13,6 @@ import (
"net"
"nhooyr.io/websocket"
"tailscale.com/net/wsconn"
)
func init() {
@@ -29,6 +28,6 @@ func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
return nil, err
}
log.Printf("websocket: connected to %v", urlStr)
netConn := wsconn.NetConn(context.Background(), c, websocket.MessageBinary)
netConn := websocket.NetConn(context.Background(), c, websocket.MessageBinary)
return netConn, nil
}

View File

@@ -15,9 +15,9 @@
// The recipient then decrypts the bytes following (the nacl secretbox)
// and then the inner payload structure is:
//
// messageType byte (the MessageType constants below)
// messageVersion byte (0 for now; but always ignore bytes at the end)
// message-payload [...]byte
// messageType byte (the MessageType constants below)
// messageVersion byte (0 for now; but always ignore bytes at the end)
// message-paylod [...]byte
package disco
import (

View File

@@ -9,7 +9,7 @@ spec:
serviceAccountName: "{{SA_NAME}}"
initContainers:
# In order to run as a proxy we need to enable IP Forwarding inside
# the container. The `net.ipv4.ip_forward` sysctl is not allowlisted
# the container. The `net.ipv4.ip_forward` sysctl is not whitelisted
# in Kubelet by default.
- name: sysctler
image: busybox
@@ -18,7 +18,7 @@ spec:
command: ["/bin/sh"]
args:
- -c
- sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
- sysctl -w net.ipv4.ip_forward=1 -w net.ipv6.conf.all.forwarding=1
resources:
requests:
cpu: 1m

View File

@@ -277,11 +277,6 @@ func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }
// TKASkipSignatureCheck is whether to skip node-key signature checking for development.
func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION") }
// NoLogsNoSupport reports whether the client's opted out of log uploads and
// technical support.
func NoLogsNoSupport() bool {

9
go.mod
View File

@@ -44,7 +44,7 @@ require (
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
@@ -57,9 +57,9 @@ require (
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
golang.org/x/net v0.0.0-20221002022538-bcab6841153b
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.11
@@ -210,6 +210,7 @@ require (
github.com/nishanths/exhaustive v0.7.11 // indirect
github.com/nishanths/predeclared v0.2.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
@@ -229,7 +230,7 @@ require (
github.com/ryancurrah/gomodguard v1.2.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sassoftware/go-rpmutils v0.1.0 // indirect
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
github.com/securego/gosec/v2 v2.9.3 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect

16
go.sum
View File

@@ -876,6 +876,7 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@@ -989,9 +990,8 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc=
github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/sassoftware/go-rpmutils v0.1.0 h1:VLrna+tV+77Tclr956QkY/pTyyKomQlq2Xw6PuE8tsc=
github.com/sassoftware/go-rpmutils v0.1.0/go.mod h1:euhXULoBpvAxqrBHEyJS4Tsu3hHxUmQWNymxoJbzgUY=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo=
github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc=
@@ -1082,8 +1082,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986 h1:jWSwTR9CY13oa2oxhR3FInk1ybqC1NbF9cFeoWrrx+E=
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f h1:n4r/sJ92cBSBHK8n9lR1XLFr0OiTVeGfN5TR+9LaN7E=
@@ -1235,7 +1235,6 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@@ -1355,8 +1354,8 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1491,9 +1490,8 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=

View File

@@ -1 +1 @@
3fd24dee31726924c1b61c8037a889b30b8aa0f6
b13188dd36c1ad2509796ce10b6a1231b200c36a

View File

@@ -67,9 +67,9 @@ type Notify struct {
LoginFinished *empty.Message // non-nil when/if the login process succeeded
State *State // if non-nil, the new or current IPN state
Prefs *PrefsView // if non-nil && Valid, the new or current preferences
Prefs *Prefs // if non-nil, the new or current preferences
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
Engine *EngineStatus // if non-nil, the new or current wireguard stats
Engine *EngineStatus // if non-nil, the new or urrent wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
BackendLogID *string // if non-nil, the public logtail ID used by backend
@@ -106,7 +106,7 @@ func (n Notify) String() string {
if n.State != nil {
fmt.Fprintf(&sb, "state=%v ", *n.State)
}
if n.Prefs.Valid() {
if n.Prefs != nil {
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
}
if n.NetMap != nil {
@@ -168,11 +168,6 @@ type PartialFile struct {
// LocalBackend.userID, a string like "user-$USER_ID" (used in
// server mode).
// - on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
//
// Additionally, the StateKey can be debug setting name:
//
// - "_debug_magicsock_until" with value being a unix timestamp stringified
// - "_debug_<component>_until" with value being a unix timestamp stringified
type StateKey string
type Options struct {

View File

@@ -22,8 +22,7 @@ func (b *FakeBackend) Start(opts Options) error {
}
nl := NeedsLogin
if b.notify != nil {
p := opts.Prefs.View()
b.notify(Notify{Prefs: &p})
b.notify(Notify{Prefs: opts.Prefs})
b.notify(Notify{State: &nl})
}
return nil
@@ -84,8 +83,7 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
}
if b.notify != nil {
p := new.View()
b.notify(Notify{Prefs: &p})
b.notify(Notify{Prefs: new.Clone()})
}
if new.WantRunning && !b.live {
b.newState(Starting)

176
ipn/handle.go Normal file
View File

@@ -0,0 +1,176 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
import (
"net/netip"
"sync"
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
)
type Handle struct {
b Backend
logf logger.Logf
// Mutex protects everything below
mu sync.Mutex
xnotify func(Notify)
frontendLogID string
netmapCache *netmap.NetworkMap
engineStatusCache EngineStatus
stateCache State
prefsCache *Prefs
}
func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (*Handle, error) {
h := &Handle{
b: b,
logf: logf,
}
h.SetNotifyCallback(notify)
err := h.Start(opts)
if err != nil {
return nil, err
}
return h, nil
}
func (h *Handle) SetNotifyCallback(notify func(Notify)) {
h.mu.Lock()
h.xnotify = notify
h.mu.Unlock()
h.b.SetNotifyCallback(h.notify)
}
func (h *Handle) Start(opts Options) error {
h.mu.Lock()
h.frontendLogID = opts.FrontendLogID
h.netmapCache = nil
h.engineStatusCache = EngineStatus{}
h.stateCache = NoState
if opts.Prefs != nil {
h.prefsCache = opts.Prefs.Clone()
}
h.mu.Unlock()
return h.b.Start(opts)
}
func (h *Handle) Reset() {
st := NoState
h.notify(Notify{State: &st})
}
func (h *Handle) notify(n Notify) {
h.mu.Lock()
if n.BackendLogID != nil {
h.logf("Handle: logs: be:%v fe:%v",
*n.BackendLogID, h.frontendLogID)
}
if n.State != nil {
h.stateCache = *n.State
}
if n.Prefs != nil {
h.prefsCache = n.Prefs.Clone()
}
if n.NetMap != nil {
h.netmapCache = n.NetMap
}
if n.Engine != nil {
h.engineStatusCache = *n.Engine
}
h.mu.Unlock()
if h.xnotify != nil {
// Forward onward to our parent's notifier
h.xnotify(n)
}
}
func (h *Handle) Prefs() *Prefs {
h.mu.Lock()
defer h.mu.Unlock()
return h.prefsCache.Clone()
}
func (h *Handle) UpdatePrefs(updateFn func(p *Prefs)) {
h.mu.Lock()
defer h.mu.Unlock()
new := h.prefsCache.Clone()
updateFn(new)
h.prefsCache = new
h.b.SetPrefs(new)
}
func (h *Handle) State() State {
h.mu.Lock()
defer h.mu.Unlock()
return h.stateCache
}
func (h *Handle) EngineStatus() EngineStatus {
h.mu.Lock()
defer h.mu.Unlock()
return h.engineStatusCache
}
func (h *Handle) LocalAddrs() []netip.Prefix {
h.mu.Lock()
defer h.mu.Unlock()
nm := h.netmapCache
if nm != nil {
return nm.Addresses
}
return []netip.Prefix{}
}
func (h *Handle) NetMap() *netmap.NetworkMap {
h.mu.Lock()
defer h.mu.Unlock()
return h.netmapCache
}
func (h *Handle) Expiry() time.Time {
h.mu.Lock()
defer h.mu.Unlock()
nm := h.netmapCache
if nm != nil {
return nm.Expiry
}
return time.Time{}
}
func (h *Handle) AdminPageURL() string {
return h.prefsCache.AdminPageURL()
}
func (h *Handle) StartLoginInteractive() {
h.b.StartLoginInteractive()
}
func (h *Handle) Login(token *tailcfg.Oauth2Token) {
h.b.Login(token)
}
func (h *Handle) Logout() {
h.b.Logout()
}
func (h *Handle) RequestEngineStatus() {
h.b.RequestEngineStatus()
}

View File

@@ -1,120 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package ipn
import (
"encoding/json"
"errors"
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs
// View returns a readonly view of Prefs.
func (p *Prefs) View() PrefsView {
return PrefsView{ж: p}
}
// PrefsView provides a read-only view over Prefs.
//
// Its methods should only be called if `Valid()` returns true.
type PrefsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Prefs
}
// Valid reports whether underlying value is non-nil.
func (v PrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v PrefsView) AsStruct() *Prefs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v PrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *PrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Prefs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v PrefsView) ControlURL() string { return v.ж.ControlURL }
func (v PrefsView) RouteAll() bool { return v.ж.RouteAll }
func (v PrefsView) AllowSingleHosts() bool { return v.ж.AllowSingleHosts }
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess }
func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS }
func (v PrefsView) RunSSH() bool { return v.ж.RunSSH }
func (v PrefsView) WantRunning() bool { return v.ж.WantRunning }
func (v PrefsView) LoggedOut() bool { return v.ж.LoggedOut }
func (v PrefsView) ShieldsUp() bool { return v.ж.ShieldsUp }
func (v PrefsView) AdvertiseTags() views.Slice[string] { return views.SliceOf(v.ж.AdvertiseTags) }
func (v PrefsView) Hostname() string { return v.ж.Hostname }
func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs }
func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon }
func (v PrefsView) Egg() bool { return v.ж.Egg }
func (v PrefsView) AdvertiseRoutes() views.IPPrefixSlice {
return views.IPPrefixSliceOf(v.ж.AdvertiseRoutes)
}
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
func (v PrefsView) Persist() *persist.Persist {
if v.ж.Persist == nil {
return nil
}
x := *v.ж.Persist
return &x
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsViewNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr
ExitNodeAllowLANAccess bool
CorpDNS bool
RunSSH bool
WantRunning bool
LoggedOut bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
NotepadURLs bool
ForceDaemon bool
Egg bool
AdvertiseRoutes []netip.Prefix
NoSNAT bool
NetfilterMode preftype.NetfilterMode
OperatorUser string
Persist *persist.Persist
}{})

View File

@@ -8,8 +8,6 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"time"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
@@ -34,21 +32,6 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
case "/debug/metrics":
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
case "/debug/component-logging":
component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs"))
if secs == 0 {
secs -= 1
}
until := time.Now().Add(time.Duration(secs) * time.Second)
err := b.SetComponentDebugLogging(component, until)
var res struct {
Error string `json:",omitempty"`
}
if err != nil {
res.Error = err.Error()
}
writeJSON(res)
case "/ssh/usernames":
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {

View File

@@ -314,7 +314,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
verOS = "linux"
}
var log tstest.MemLogger
got := dnsConfigForNetmap(tt.nm, tt.prefs.View(), log.Logf, verOS)
got := dnsConfigForNetmap(tt.nm, tt.prefs, log.Logf, verOS)
if !reflect.DeepEqual(got, tt.want) {
gotj, _ := json.MarshalIndent(got, "", "\t")
wantj, _ := json.MarshalIndent(tt.want, "", "\t")

File diff suppressed because it is too large Load Diff

View File

@@ -62,7 +62,7 @@ func TestLocalLogLines(t *testing.T) {
defer lb.Shutdown()
// custom adjustments for required non-nil fields
lb.prefs = ipn.NewPrefs().View()
lb.prefs = ipn.NewPrefs()
lb.hostinfo = &tailcfg.Hostinfo{}
// hacky manual override of the usual log-on-change behaviour of keylogf
lb.keyLogf = logListen.Logf

View File

@@ -17,8 +17,8 @@ import (
"time"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
@@ -26,83 +26,36 @@ import (
"tailscale.com/types/tkatype"
)
// TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
var (
errMissingNetmap = errors.New("missing netmap: verify that you are logged in")
errNetworkLockNotActive = errors.New("network-lock is not active")
)
var networkLockAvailable = envknob.RegisterBool("TS_EXPERIMENTAL_NETWORK_LOCK")
type tkaState struct {
authority *tka.Authority
storage *tka.FS
}
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
// nodes from the netmap who's signature does not verify.
//
// b.mu must be held.
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
if !envknob.UseWIPCode() {
return // Feature-flag till network-lock is in Alpha.
}
if b.tka == nil {
return // TKA not enabled.
}
toDelete := make(map[int]struct{}, len(nm.Peers))
for i, p := range nm.Peers {
if len(p.KeySignature) == 0 {
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID)
toDelete[i] = struct{}{}
} else {
if err := b.tka.authority.NodeKeyAuthorized(p.Key, p.KeySignature); err != nil {
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID, p.StableID, err)
toDelete[i] = struct{}{}
}
}
}
// nm.Peers is ordered, so deletion must be order-preserving.
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
for i, p := range nm.Peers {
if _, delete := toDelete[i]; !delete {
peers = append(peers, p)
}
}
nm.Peers = peers
}
// tkaSyncIfNeeded examines TKA info reported from the control plane,
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
// performing the steps necessary to synchronize local tka state.
//
// There are 4 scenarios handled here:
// - Enablement: nm.TKAEnabled but b.tka == nil
// ∴ reach out to /machine/tka/bootstrap to get the genesis AUM, then
// ∴ reach out to /machine/tka/boostrap to get the genesis AUM, then
// initialize TKA.
// - Disablement: !nm.TKAEnabled but b.tka != nil
// ∴ reach out to /machine/tka/bootstrap to read the disablement secret,
// ∴ reach out to /machine/tka/boostrap to read the disablement secret,
// then verify and clear tka local state.
// - Sync needed: b.tka.Head != nm.TKAHead
// ∴ complete multi-step synchronization flow.
// - Everything up to date: All other cases.
// ∴ no action necessary.
//
// tkaSyncIfNeeded immediately takes b.takeSyncLock which is held throughout,
// and may take b.mu as required.
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsView) error {
if !envknob.UseWIPCode() {
// b.mu must be held. b.mu will be stepped out of (and back in) during network
// RPCs.
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
if !networkLockAvailable() {
// If the feature flag is not enabled, pretend we don't exist.
return nil
}
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
defer b.tkaSyncLock.Unlock()
b.mu.Lock() // take mu to protect access to synchronized fields.
defer b.mu.Unlock()
ourNodeKey := prefs.Persist().PublicNodeKey()
isEnabled := b.tka != nil
wantEnabled := nm.TKAEnabled
if isEnabled != wantEnabled {
@@ -113,16 +66,17 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
// Regardless of whether we are moving to disabled or enabled, we
// need information from the tka bootstrap endpoint.
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
b.mu.Unlock()
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
b.mu.Lock()
if err != nil {
return fmt.Errorf("fetching bootstrap: %w", err)
return fmt.Errorf("fetching bootstrap: %v", err)
}
if wantEnabled && !isEnabled {
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
return fmt.Errorf("bootstrap: %w", err)
return fmt.Errorf("bootstrap: %v", err)
}
isEnabled = true
} else if !wantEnabled && isEnabled {
@@ -142,98 +96,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
}
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
if err := b.tkaSyncLocked(ourNodeKey); err != nil {
return fmt.Errorf("tka sync: %w", err)
}
}
return nil
}
func toSyncOffer(head string, ancestors []string) (tka.SyncOffer, error) {
var out tka.SyncOffer
if err := out.Head.UnmarshalText([]byte(head)); err != nil {
return tka.SyncOffer{}, fmt.Errorf("head.UnmarshalText: %v", err)
}
out.Ancestors = make([]tka.AUMHash, len(ancestors))
for i, a := range ancestors {
if err := out.Ancestors[i].UnmarshalText([]byte(a)); err != nil {
return tka.SyncOffer{}, fmt.Errorf("ancestor[%d].UnmarshalText: %v", i, err)
}
}
return out, nil
}
// tkaSyncLocked synchronizes TKA state with control. b.mu must be held
// and tka must be initialized. b.mu will be stepped out of (and back into)
// during network RPCs.
//
// b.mu must be held.
func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
offer, err := b.tka.authority.SyncOffer(b.tka.storage)
if err != nil {
return fmt.Errorf("offer: %w", err)
}
b.mu.Unlock()
offerResp, err := b.tkaDoSyncOffer(ourNodeKey, offer)
b.mu.Lock()
if err != nil {
return fmt.Errorf("offer RPC: %w", err)
}
controlOffer, err := toSyncOffer(offerResp.Head, offerResp.Ancestors)
if err != nil {
return fmt.Errorf("control offer: %v", err)
}
if controlOffer.Head == offer.Head {
// We are up to date.
return nil
}
// Compute missing AUMs before we apply any AUMs from the control-plane,
// so we still submit AUMs to control even if they are not part of the
// active chain.
toSendAUMs, err := b.tka.authority.MissingAUMs(b.tka.storage, controlOffer)
if err != nil {
return fmt.Errorf("computing missing AUMs: %w", err)
}
// If we got this far, then we are not up to date. Either the control-plane
// has updates for us, or we have updates for the control plane.
//
// TODO(tom): Do we want to keep processing even if the Inform fails? Need
// to think through if theres holdback concerns here or not.
if len(offerResp.MissingAUMs) > 0 {
aums := make([]tka.AUM, len(offerResp.MissingAUMs))
for i, a := range offerResp.MissingAUMs {
if err := aums[i].Unserialize(a); err != nil {
return fmt.Errorf("MissingAUMs[%d]: %v", i, err)
}
}
if err := b.tka.authority.Inform(b.tka.storage, aums); err != nil {
return fmt.Errorf("inform failed: %v", err)
}
}
// NOTE(tom): We could short-circuit here if our HEAD equals the
// control-plane's head, but we don't just so control always has a
// copy of all forks that clients had.
b.mu.Unlock()
sendResp, err := b.tkaDoSyncSend(ourNodeKey, toSendAUMs, false)
b.mu.Lock()
if err != nil {
return fmt.Errorf("send RPC: %v", err)
}
var remoteHead tka.AUMHash
if err := remoteHead.UnmarshalText([]byte(sendResp.Head)); err != nil {
return fmt.Errorf("head unmarshal: %v", err)
}
if remoteHead != b.tka.authority.Head() {
b.logf("TKA desync: expected consensus after sync but our head is %v and the control plane's is %v", b.tka.authority.Head(), remoteHead)
// TODO(tom): Implement sync
}
return nil
@@ -250,8 +113,8 @@ func (b *LocalBackend) chonkPath() string {
//
// b.mu must be held.
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
if err := b.CanSupportNetworkLock(); err != nil {
return err
if !b.CanSupportNetworkLock() {
return errors.New("network lock not supported in this configuration")
}
var genesis tka.AUM
@@ -280,34 +143,26 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
return nil
}
// CanSupportNetworkLock returns nil if tailscaled is able to operate
// CanSupportNetworkLock returns true if tailscaled is able to operate
// a local tailnet key authority (and hence enforce network lock).
func (b *LocalBackend) CanSupportNetworkLock() error {
if !envknob.UseWIPCode() {
return errors.New("this feature is not yet complete, a later release may support this functionality")
}
func (b *LocalBackend) CanSupportNetworkLock() bool {
if b.tka != nil {
// If the TKA is being used, it is supported.
return nil
// The TKA is being used, so yeah its supported.
return true
}
if b.TailscaleVarRoot() == "" {
return errors.New("network-lock is not supported in this configuration, try setting --statedir")
if b.TailscaleVarRoot() != "" {
// Theres a var root (aka --statedir), so if network lock gets
// initialized we have somewhere to store our AUMs. Thats all
// we need.
return true
}
// There's a var root (aka --statedir), so if network lock gets
// initialized we have somewhere to store our AUMs. That's all
// we need.
return nil
return false
}
// NetworkLockStatus returns a structure describing the state of the
// tailnet key authority, if any.
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return &ipnstate.NetworkLockStatus{
Enabled: false,
@@ -336,14 +191,20 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
// The Finish RPC submits signatures for all these nodes, at which point
// Control has everything it needs to atomically enable network lock.
func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
if err := b.CanSupportNetworkLock(); err != nil {
return err
if b.tka != nil {
return errors.New("network-lock is already initialized")
}
if !networkLockAvailable() {
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
}
if !b.CanSupportNetworkLock() {
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
}
var ourNodeKey key.NodePublic
b.mu.Lock()
if b.prefs.Valid() {
ourNodeKey = b.prefs.Persist().PublicNodeKey()
if b.prefs != nil {
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
}
b.mu.Unlock()
if ourNodeKey.IsZero() {
@@ -394,87 +255,6 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
return err
}
// Only use is in tests.
func (b *LocalBackend) NetworkLockVerifySignatureForTest(nks tkatype.MarshaledSignature, nodeKey key.NodePublic) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return errNetworkLockNotActive
}
return b.tka.authority.NodeKeyAuthorized(nodeKey, nks)
}
// Only use is in tests.
func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
panic("network lock not initialized")
}
return b.tka.authority.KeyTrusted(keyID)
}
// NetworkLockModify adds and/or removes keys in the tailnet's key authority.
func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("modify network-lock keys: %w", err)
}
}()
b.mu.Lock()
defer b.mu.Unlock()
if err := b.CanSupportNetworkLock(); err != nil {
return err
}
if b.tka == nil {
return errNetworkLockNotActive
}
updater := b.tka.authority.NewUpdater(b.nlPrivKey)
for _, addKey := range addKeys {
if err := updater.AddKey(addKey); err != nil {
return err
}
}
for _, removeKey := range removeKeys {
if err := updater.RemoveKey(removeKey.ID()); err != nil {
return err
}
}
aums, err := updater.Finalize(b.tka.storage)
if err != nil {
return err
}
if len(aums) == 0 {
return nil
}
ourNodeKey := b.prefs.Persist().PublicNodeKey()
b.mu.Unlock()
resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true)
b.mu.Lock()
if err != nil {
return err
}
var controlHead tka.AUMHash
if err := controlHead.UnmarshalText([]byte(resp.Head)); err != nil {
return err
}
lastHead := aums[len(aums)-1].Hash()
if controlHead != lastHead {
return errors.New("central tka head differs from submitted AUM, try again")
}
return nil
}
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
p, err := nodeInfo.NodePublic.MarshalBinary()
if err != nil {
@@ -506,27 +286,34 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
bo := backoff.NewBackoff("tka-init-begin", b.logf, 5*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("ctx: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitBeginResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitBeginResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
return a, nil
}
}
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
@@ -541,28 +328,34 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
bo := backoff.NewBackoff("tka-init-finish", b.logf, 5*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("ctx: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitFinishResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitFinishResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
return a, nil
}
}
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
@@ -612,107 +405,3 @@ func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUM
return a, nil
}
func fromSyncOffer(offer tka.SyncOffer) (head string, ancestors []string, err error) {
headBytes, err := offer.Head.MarshalText()
if err != nil {
return "", nil, fmt.Errorf("head.MarshalText: %v", err)
}
ancestors = make([]string, len(offer.Ancestors))
for i, ancestor := range offer.Ancestors {
hash, err := ancestor.MarshalText()
if err != nil {
return "", nil, fmt.Errorf("ancestor[%d].MarshalText: %v", i, err)
}
ancestors[i] = string(hash)
}
return string(headBytes), ancestors, nil
}
// tkaDoSyncOffer sends a /machine/tka/sync/offer RPC to the control plane
// over noise. This is the first of two RPCs implementing tka synchronization.
func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncOffer) (*tailcfg.TKASyncOfferResponse, error) {
head, ancestors, err := fromSyncOffer(offer)
if err != nil {
return nil, fmt.Errorf("encoding offer: %v", err)
}
syncReq := tailcfg.TKASyncOfferRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
Head: head,
Ancestors: ancestors,
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(syncReq); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/offer", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKASyncOfferResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}
// tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane
// over noise. This is the second of two RPCs implementing tka synchronization.
func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) {
sendReq := tailcfg.TKASyncSendRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
Interactive: interactive,
}
for i, a := range aums {
sendReq.MissingAUMs[i] = a.Serialize()
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(sendReq); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/send", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKASyncSendResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}

View File

@@ -15,9 +15,7 @@ import (
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
@@ -25,7 +23,6 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/tkatype"
)
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
@@ -52,6 +49,8 @@ func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
return cc
}
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
// httptest plumbing, despite the domain being unused in the actual noise request transport.
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
ts := httptest.NewUnstartedServer(handler)
ts.StartTLS()
@@ -64,7 +63,7 @@ func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server,
}
func TestTKAEnablementFlow(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
networkLockAvailable = func() bool { return true } // Enable the feature flag
nodePriv := key.NewNode()
// Make a fake TKA authority, getting a usable genesis AUM which
@@ -105,9 +104,6 @@ func TestTKAEnablementFlow(t *testing.T) {
t.Fatal(err)
}
case "/machine/tka/sync/offer", "/machine/tka/sync/send":
t.Error("node attempted to sync, but should have been up to date")
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
@@ -122,15 +118,17 @@ func TestTKAEnablementFlow(t *testing.T) {
cc: cc,
ccAuto: cc,
logf: t.Logf,
prefs: (&ipn.Prefs{
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View(),
},
}
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
TKAEnabled: true,
TKAHead: a1.Head(),
}, b.prefs)
TKAHead: tka.AUMHash{},
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@@ -143,7 +141,7 @@ func TestTKAEnablementFlow(t *testing.T) {
}
func TestTKADisablementFlow(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
networkLockAvailable = func() bool { return true } // Enable the feature flag
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
@@ -219,17 +217,19 @@ func TestTKADisablementFlow(t *testing.T) {
authority: authority,
storage: chonk,
},
prefs: (&ipn.Prefs{
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View(),
},
}
// Test that the wrong disablement secret does not shut down the authority.
returnWrongSecret = true
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
TKAEnabled: false,
TKAHead: authority.Head(),
}, b.prefs)
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@@ -239,10 +239,12 @@ func TestTKADisablementFlow(t *testing.T) {
// Test the correct disablement secret shuts down the authority.
returnWrongSecret = false
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
TKAEnabled: false,
TKAHead: authority.Head(),
}, b.prefs)
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@@ -254,285 +256,3 @@ func TestTKADisablementFlow(t *testing.T) {
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
}
}
func TestTKASync(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
someKeyPriv := key.NewNLPrivate()
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
type tkaSyncScenario struct {
name string
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
// on control should be seeded with.
controlAUMs func(*testing.T, *tka.Authority, tka.Chonk, tka.Signer) []tka.AUM
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
// on the node should be seeded with.
nodeAUMs func(*testing.T, *tka.Authority, tka.Chonk, tka.Signer) []tka.AUM
}
tcs := []tkaSyncScenario{
{name: "up to date"},
{
name: "control has an update",
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.RemoveKey(someKey.ID()); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
},
{
// AKA 'control data loss' scenario
name: "node has an update",
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.RemoveKey(someKey.ID()); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
},
{
// AKA 'control data loss + update in the meantime' scenario
name: "node and control diverge",
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swiggity"}); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swooty"}); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
nlPriv := key.NewNLPrivate()
// Setup the tka authority on the control plane.
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
controlStorage := &tka.Mem{}
controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{
Keys: []tka.Key{key, someKey},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
if tc.controlAUMs != nil {
if err := controlAuthority.Inform(controlStorage, tc.controlAUMs(t, controlAuthority, controlStorage, nlPriv)); err != nil {
t.Fatalf("controlAuthority.Inform() failed: %v", err)
}
}
// Setup the TKA authority on the node.
nodeStorage, err := tka.ChonkDir(filepath.Join(temp, "tka"))
if err != nil {
t.Fatal(err)
}
nodeAuthority, err := tka.Bootstrap(nodeStorage, bootstrap)
if err != nil {
t.Fatalf("tka.Bootstrap() failed: %v", err)
}
if tc.nodeAUMs != nil {
if err := nodeAuthority.Inform(nodeStorage, tc.nodeAUMs(t, nodeAuthority, nodeStorage, nlPriv)); err != nil {
t.Fatalf("nodeAuthority.Inform() failed: %v", err)
}
}
// Make a mock control server.
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/sync/offer":
body := new(tailcfg.TKASyncOfferRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
t.Logf("got sync offer:\n%+v", body)
nodeOffer, err := toSyncOffer(body.Head, body.Ancestors)
if err != nil {
t.Fatal(err)
}
controlOffer, err := controlAuthority.SyncOffer(controlStorage)
if err != nil {
t.Fatal(err)
}
sendAUMs, err := controlAuthority.MissingAUMs(controlStorage, nodeOffer)
if err != nil {
t.Fatal(err)
}
head, ancestors, err := fromSyncOffer(controlOffer)
if err != nil {
t.Fatal(err)
}
resp := tailcfg.TKASyncOfferResponse{
Head: head,
Ancestors: ancestors,
MissingAUMs: make([]tkatype.MarshaledAUM, len(sendAUMs)),
}
for i, a := range sendAUMs {
resp.MissingAUMs[i] = a.Serialize()
}
t.Logf("responding to sync offer with:\n%+v", resp)
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(resp); err != nil {
t.Fatal(err)
}
case "/machine/tka/sync/send":
body := new(tailcfg.TKASyncSendRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
t.Logf("got sync send:\n%+v", body)
toApply := make([]tka.AUM, len(body.MissingAUMs))
for i, a := range body.MissingAUMs {
if err := toApply[i].Unserialize(a); err != nil {
t.Fatalf("decoding missingAUM[%d]: %v", i, err)
}
}
if len(toApply) > 0 {
if err := controlAuthority.Inform(controlStorage, toApply); err != nil {
t.Fatalf("control.Inform(%+v) failed: %v", toApply, err)
}
}
head, err := controlAuthority.Head().MarshalText()
if err != nil {
t.Fatal(err)
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{Head: string(head)}); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
// Setup the client.
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: nodeAuthority,
storage: nodeStorage,
},
prefs: (&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View(),
}
// Finally, lets trigger a sync.
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
TKAEnabled: true,
TKAHead: controlAuthority.Head(),
}, b.prefs)
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
// Check that at the end of this ordeal, the node and the control
// plane are in sync.
if nodeHead, controlHead := b.tka.authority.Head(), controlAuthority.Head(); nodeHead != controlHead {
t.Errorf("node head = %v, want %v", nodeHead, controlHead)
}
})
}
}
func TestTKAFilterNetmap(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
nlPriv := key.NewNLPrivate()
nlKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
storage := &tka.Mem{}
authority, _, err := tka.Create(storage, tka.State{
Keys: []tka.Key{nlKey},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode()
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
if err != nil {
t.Fatal(err)
}
n4Sig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n4.Public()}, nlPriv)
if err != nil {
t.Fatal(err)
}
n4Sig.Signature[3] = 42 // mess up the signature
n4Sig.Signature[4] = 42 // mess up the signature
n5GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n5.Public()}, nlPriv)
if err != nil {
t.Fatal(err)
}
nm := netmap.NetworkMap{
Peers: []*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
},
}
b := &LocalBackend{
logf: t.Logf,
tka: &tkaState{authority: authority},
}
b.tkaFilterNetmapLocked(&nm)
want := []*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
}
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
return x.Raw32() == y.Raw32()
})
if diff := cmp.Diff(nm.Peers, want, nodePubComparer); diff != "" {
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
}
}

View File

@@ -32,7 +32,6 @@ import (
"unicode/utf8"
"github.com/kortschak/wol"
"golang.org/x/exp/slices"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/health"
@@ -59,6 +58,7 @@ var addH2C func(*http.Server)
type peerAPIServer struct {
b *LocalBackend
rootDir string // empty means file receiving unavailable
selfNode *tailcfg.Node
knownEmpty atomic.Bool
resolver *resolver.Resolver
@@ -79,7 +79,7 @@ type peerAPIServer struct {
}
const (
// partialSuffix is the suffix appended to files while they're
// partialSuffix is the suffix appened to files while they're
// still in the process of being transferred.
partialSuffix = ".partial"
@@ -512,17 +512,10 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
c.Close()
return
}
nm := pln.lb.NetMap()
if nm == nil || nm.SelfNode == nil {
logf("peerapi: no netmap")
c.Close()
return
}
h := &peerAPIHandler{
ps: pln.ps,
isSelf: nm.SelfNode.User == peerNode.User,
isSelf: pln.ps.selfNode.User == peerNode.User,
remoteAddr: src,
selfNode: nm.SelfNode,
peerNode: peerNode,
peerUser: peerUser,
}
@@ -540,7 +533,6 @@ type peerAPIHandler struct {
ps *peerAPIServer
remoteAddr netip.AddrPort
isSelf bool // whether peerNode is owned by same user as this node
selfNode *tailcfg.Node // this node; always non-nil
peerNode *tailcfg.Node // peerNode is who's making the request
peerUser tailcfg.UserProfile // profile of peerNode
}
@@ -549,37 +541,7 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
h.ps.b.logf("peerapi: "+format, a...)
}
func (h *peerAPIHandler) validateHost(r *http.Request) error {
if r.Host == "peer" {
return nil
}
ap, err := netip.ParseAddrPort(r.Host)
if err != nil {
return err
}
hostIPPfx := netip.PrefixFrom(ap.Addr(), ap.Addr().BitLen())
if !slices.Contains(h.selfNode.Addresses, hostIPPfx) {
return fmt.Errorf("%v not found in self addresses", hostIPPfx)
}
return nil
}
func (h *peerAPIHandler) validatePeerAPIRequest(r *http.Request) error {
if r.Referer() != "" {
return errors.New("unexpected Referer")
}
if r.Header.Get("Origin") != "" {
return errors.New("unexpected Origin")
}
return h.validateHost(r)
}
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.validatePeerAPIRequest(r); err != nil {
h.logf("invalid request from %v: %v", h.remoteAddr, err)
http.Error(w, "invalid peerapi request", http.StatusForbidden)
return
}
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
h.handlePeerPut(w, r)
return
@@ -724,10 +686,6 @@ func (h *peerAPIHandler) canPutFile() bool {
// canDebug reports whether h can debug this node (goroutines, metrics,
// magicsock internal state, etc).
func (h *peerAPIHandler) canDebug() bool {
if !slices.Contains(h.selfNode.Capabilities, tailcfg.CapabilityDebug) {
// This node does not expose debug info.
return false
}
return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
}
@@ -1226,7 +1184,7 @@ func newFakePeerAPIListener(ip netip.Addr) net.Listener {
// even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
// or we lack permission to listen on a port. It's okay to not actually listen via
// the kernel because on almost all platforms (except iOS as of 2022-04-20) we
// also intercept incoming netstack TCP requests to our peerapi port and hand them over
// also intercept netstack TCP requests in to our peerapi port and hand it over
// directly to peerapi, without involving the kernel. So this doesn't need to be
// real. But the port number we return (1, in this case) is the port number we advertise
// to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's

View File

@@ -24,7 +24,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
@@ -107,12 +106,10 @@ func hexAll(v string) string {
}
func TestHandlePeerAPI(t *testing.T) {
const nodeFQDN = "self-node.tail-scale.ts.net."
tests := []struct {
name string
isSelf bool // the peer sending the request is owned by us
capSharing bool // self node has file sharing capability
debugCap bool // self node has debug capability
capSharing bool // self node has file sharing capabilty
omitRoot bool // don't configure
req *http.Request
checks []check
@@ -140,24 +137,15 @@ func TestHandlePeerAPI(t *testing.T) {
),
},
{
name: "goroutines/deny-self-no-cap",
isSelf: true,
debugCap: false,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
checks: checks(httpStatus(403)),
name: "peer_api_goroutines_deny",
isSelf: false,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
checks: checks(httpStatus(403)),
},
{
name: "goroutines/deny-nonself",
isSelf: false,
debugCap: true,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
checks: checks(httpStatus(403)),
},
{
name: "goroutines/accept-self",
isSelf: true,
debugCap: true,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
name: "peer_api_goroutines",
isSelf: true,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
checks: checks(
httpStatus(200),
bodyContains("ServeHTTP"),
@@ -412,53 +400,16 @@ func TestHandlePeerAPI(t *testing.T) {
bodyContains("bad filename"),
),
},
{
name: "host-val/bad-ip",
isSelf: true,
debugCap: true,
req: httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil),
checks: checks(
httpStatus(403),
),
},
{
name: "host-val/no-port",
isSelf: true,
debugCap: true,
req: httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil),
checks: checks(
httpStatus(403),
),
},
{
name: "host-val/peer",
isSelf: true,
debugCap: true,
req: httptest.NewRequest("GET", "http://peer/v0/env", nil),
checks: checks(
httpStatus(200),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selfNode := &tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.100.100.101/32"),
},
}
if tt.debugCap {
selfNode.Capabilities = append(selfNode.Capabilities, tailcfg.CapabilityDebug)
}
var e peerAPITestEnv
lb := &LocalBackend{
logf: e.logBuf.Logf,
capFileSharing: tt.capSharing,
netMap: &netmap.NetworkMap{SelfNode: selfNode},
}
e.ph = &peerAPIHandler{
isSelf: tt.isSelf,
selfNode: selfNode,
isSelf: tt.isSelf,
peerNode: &tailcfg.Node{
ComputedName: "some-peer-name",
},
@@ -472,9 +423,6 @@ func TestHandlePeerAPI(t *testing.T) {
e.ph.ps.rootDir = rootDir
}
e.rr = httptest.NewRecorder()
if tt.req.Host == "example.com" {
tt.req.Host = "100.100.100.101:12345"
}
e.ph.ServeHTTP(e.rr, tt.req)
for _, f := range tt.checks {
f(t, &e)
@@ -512,15 +460,12 @@ func TestFileDeleteRace(t *testing.T) {
peerNode: &tailcfg.Node{
ComputedName: "some-peer-name",
},
selfNode: &tailcfg.Node{
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
},
ps: ps,
}
buf := make([]byte, 2<<20)
for i := 0; i < 30; i++ {
rr := httptest.NewRecorder()
ph.ServeHTTP(rr, httptest.NewRequest("PUT", "http://100.100.100.101:123/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
ph.ServeHTTP(rr, httptest.NewRequest("PUT", "/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
if res := rr.Result(); res.StatusCode != 200 {
t.Fatal(res.Status)
}
@@ -648,12 +593,12 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
if h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly offering exit node")
}
h.ps.b.prefs = (&ipn.Prefs{
h.ps.b.prefs = &ipn.Prefs{
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
}).View()
}
if !h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly not offering exit node")
}

View File

@@ -46,7 +46,6 @@ func (nt *notifyThrottler) expect(count int) {
// put adds one notification into the throttler's queue.
func (nt *notifyThrottler) put(n ipn.Notify) {
nt.t.Helper()
nt.mu.Lock()
ch := nt.ch
nt.mu.Unlock()
@@ -316,7 +315,7 @@ func TestStateMachine(t *testing.T) {
return
}
if n.State != nil ||
(n.Prefs != nil && n.Prefs.Valid()) ||
n.Prefs != nil ||
n.BrowseToURL != nil ||
n.LoginFinished != nil {
logf("\n%v\n\n", n)
@@ -345,12 +344,12 @@ func TestStateMachine(t *testing.T) {
cc.assertCalls()
c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[1].State, qt.IsNotNil)
prefs := nn[0].Prefs
prefs := *nn[0].Prefs
// Note: a totally fresh system has Prefs.LoggedOut=false by
// default. We are logged out, but not because the user asked
// for it, so it doesn't count as Prefs.LoggedOut==true.
c.Assert(prefs.LoggedOut(), qt.IsFalse)
c.Assert(prefs.WantRunning(), qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -370,8 +369,8 @@ func TestStateMachine(t *testing.T) {
cc.assertCalls()
c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[1].State, qt.IsNotNil)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -396,7 +395,7 @@ func TestStateMachine(t *testing.T) {
// the user needs to visit a login URL.
t.Logf("\n\nLogin (url response)")
notifies.expect(1)
url1 := "https://localhost:1/1"
url1 := "http://localhost:1/1"
cc.send(nil, url1, false, nil)
{
cc.assertCalls("unpause")
@@ -407,8 +406,8 @@ func TestStateMachine(t *testing.T) {
nn := notifies.drain(1)
c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
}
// Now we'll try an interactive login.
@@ -442,7 +441,7 @@ func TestStateMachine(t *testing.T) {
// Provide a new interactive login URL.
t.Logf("\n\nLogin2 (url response)")
notifies.expect(1)
url2 := "https://localhost:1/2"
url2 := "http://localhost:1/2"
cc.send(nil, url2, false, nil)
{
cc.assertCalls("unpause", "unpause")
@@ -477,7 +476,7 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(nn[2].State, qt.IsNotNil)
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user1")
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user1")
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
}
@@ -577,8 +576,8 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsTrue)
c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.Stopped, qt.Equals, b.State())
c.Assert(store.sawWrite(), qt.IsTrue)
}
@@ -593,8 +592,8 @@ func TestStateMachine(t *testing.T) {
cc.assertCalls("unpause", "unpause")
c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -608,8 +607,8 @@ func TestStateMachine(t *testing.T) {
// still logged out. So it shouldn't call it again.
cc.assertCalls("StartLogout", "unpause")
cc.assertCalls()
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -621,8 +620,8 @@ func TestStateMachine(t *testing.T) {
{
notifies.drain(0)
cc.assertCalls("unpause", "unpause")
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -635,8 +634,8 @@ func TestStateMachine(t *testing.T) {
{
notifies.drain(0)
cc.assertCalls("Logout", "unpause")
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -648,8 +647,8 @@ func TestStateMachine(t *testing.T) {
{
notifies.drain(0)
cc.assertCalls("unpause", "unpause")
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -673,8 +672,8 @@ func TestStateMachine(t *testing.T) {
cc.assertCalls()
c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[1].State, qt.IsNotNil)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsTrue)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
@@ -697,9 +696,9 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(nn[2].State, qt.IsNotNil)
// Prefs after finishing the login, so LoginName updated.
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user2")
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user2")
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
}
@@ -717,7 +716,7 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
}
// One more restart, this time with a valid key, but WantRunning=false.
@@ -735,8 +734,8 @@ func TestStateMachine(t *testing.T) {
cc.assertCalls("Shutdown", "unpause", "New", "Login", "unpause")
c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[1].State, qt.IsNotNil)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(ipn.Stopped, qt.Equals, *nn[1].State)
}
@@ -797,7 +796,7 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nLoginDifferent")
notifies.expect(1)
b.StartLoginInteractive()
url3 := "https://localhost:1/3"
url3 := "http://localhost:1/3"
cc.send(nil, url3, false, nil)
{
nn := notifies.drain(1)
@@ -835,9 +834,9 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(nn[2].State, qt.IsNotNil)
// Prefs after finishing the login, so LoginName updated.
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user3")
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user3")
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
}
@@ -854,8 +853,8 @@ func TestStateMachine(t *testing.T) {
nn := notifies.drain(1)
cc.assertCalls()
c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
c.Assert(ipn.NoState, qt.Equals, b.State())
}
@@ -919,60 +918,6 @@ func TestStateMachine(t *testing.T) {
}
}
func TestEditPrefsHasNoKeys(t *testing.T) {
logf := t.Logf
store := new(testStateStorage)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(e.Close)
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
b.hostinfo = &tailcfg.Hostinfo{OS: "testos"}
b.prefs = (&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: key.NewNode(),
OldPrivateNodeKey: key.NewNode(),
LegacyFrontendPrivateMachineKey: key.NewMachine(),
},
}).View()
if b.prefs.Persist().PrivateNodeKey.IsZero() {
t.Fatalf("PrivateNodeKey not set")
}
p, err := b.EditPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{
Hostname: "foo",
},
HostnameSet: true,
})
if err != nil {
t.Fatalf("EditPrefs: %v", err)
}
if p.Hostname() != "foo" {
t.Errorf("Hostname = %q; want foo", p.Hostname())
}
// Test that we can't see the PrivateNodeKey.
if !p.Persist().PrivateNodeKey.IsZero() {
t.Errorf("PrivateNodeKey = %v; want zero", p.Persist().PrivateNodeKey)
}
// Test that we can't see the PrivateNodeKey.
if !p.Persist().OldPrivateNodeKey.IsZero() {
t.Errorf("OldPrivateNodeKey = %v; want zero", p.Persist().OldPrivateNodeKey)
}
// Test that we can't see the PrivateNodeKey.
if !p.Persist().LegacyFrontendPrivateMachineKey.IsZero() {
t.Errorf("LegacyFrontendPrivateMachineKey = %v; want zero", p.Persist().LegacyFrontendPrivateMachineKey)
}
}
type testStateStorage struct {
mem mem.Store
written atomic.Bool

View File

@@ -27,7 +27,6 @@ import (
"sync"
"syscall"
"time"
"unicode"
"go4.org/mem"
"inet.af/peercred"
@@ -58,7 +57,7 @@ import (
// Options is the configuration of the Tailscale node agent.
type Options struct {
// VarRoot is the Tailscale daemon's private writable
// VarRoot is the the Tailscale daemon's private writable
// directory (usually "/var/lib/tailscale" on Linux) that
// contains the "tailscaled.state" file, the "certs" directory
// for TLS certs, and the "files" directory for incoming
@@ -1073,20 +1072,7 @@ func (s *Server) localhostHandler(ci connIdentity) http.Handler {
})
}
// 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) {
// 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 := s.b.Status()
// TODO(bradfitz): add LogID and opts to st?

View File

@@ -26,61 +26,18 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/strs"
"tailscale.com/version"
)
type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
// handler is the set of LocalAPI handlers, keyed by the part of the
// Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash
// then it's a prefix match.
var handler = map[string]localAPIHandler{
// The prefix match handlers end with a slash:
"cert/": (*Handler).serveCert,
"file-put/": (*Handler).serveFilePut,
"files/": (*Handler).serveFiles,
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
// without a trailing slash:
"bugreport": (*Handler).serveBugReport,
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
"check-prefs": (*Handler).serveCheckPrefs,
"component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug": (*Handler).serveDebug,
"derpmap": (*Handler).serveDERPMap,
"dial": (*Handler).serveDial,
"file-targets": (*Handler).serveFileTargets,
"goroutines": (*Handler).serveGoroutines,
"id-token": (*Handler).serveIDToken,
"login-interactive": (*Handler).serveLoginInteractive,
"logout": (*Handler).serveLogout,
"metrics": (*Handler).serveMetrics,
"ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs,
"profile": (*Handler).serveProfile,
"set-dns": (*Handler).serveSetDNS,
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
"status": (*Handler).serveStatus,
"tka/init": (*Handler).serveTKAInit,
"tka/modify": (*Handler).serveTKAModify,
"tka/status": (*Handler).serveTKAStatus,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"whois": (*Handler).serveWhoIs,
}
func randHex(n int) string {
b := make([]byte, n)
rand.Read(b)
@@ -130,14 +87,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server has no local backend", http.StatusInternalServerError)
return
}
if r.Referer() != "" || r.Header.Get("Origin") != "" || !validHost(r.Host) {
http.Error(w, "invalid localapi request", http.StatusForbidden)
return
}
w.Header().Set("Tailscale-Version", version.Long)
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")
if h.RequiredPassword != "" {
_, pass, ok := r.BasicAuth()
if !ok {
@@ -149,74 +99,68 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
}
if fn, ok := handlerForPath(r.URL.Path); ok {
fn(h, w, r)
} else {
http.NotFound(w, r)
if strings.HasPrefix(r.URL.Path, "/localapi/v0/files/") {
h.serveFiles(w, r)
return
}
}
// validHost reports whether h is a valid Host header value for a LocalAPI request.
func validHost(h string) bool {
// The client code sends a hostname of "local-tailscaled.sock".
switch h {
case "", apitype.LocalAPIHost:
return true
if strings.HasPrefix(r.URL.Path, "/localapi/v0/file-put/") {
h.serveFilePut(w, r)
return
}
// Allow either localhost or loopback IP hosts.
host, portStr, err := net.SplitHostPort(h)
if err != nil {
return false
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
h.serveCert(w, r)
return
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return false
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)
case "/localapi/v0/goroutines":
h.serveGoroutines(w, r)
case "/localapi/v0/profile":
h.serveProfile(w, r)
case "/localapi/v0/status":
h.serveStatus(w, r)
case "/localapi/v0/logout":
h.serveLogout(w, r)
case "/localapi/v0/login-interactive":
h.serveLoginInteractive(w, r)
case "/localapi/v0/prefs":
h.servePrefs(w, r)
case "/localapi/v0/ping":
h.servePing(w, r)
case "/localapi/v0/check-prefs":
h.serveCheckPrefs(w, r)
case "/localapi/v0/check-ip-forwarding":
h.serveCheckIPForwarding(w, r)
case "/localapi/v0/bugreport":
h.serveBugReport(w, r)
case "/localapi/v0/file-targets":
h.serveFileTargets(w, r)
case "/localapi/v0/set-dns":
h.serveSetDNS(w, r)
case "/localapi/v0/derpmap":
h.serveDERPMap(w, r)
case "/localapi/v0/metrics":
h.serveMetrics(w, r)
case "/localapi/v0/debug":
h.serveDebug(w, r)
case "/localapi/v0/set-expiry-sooner":
h.serveSetExpirySooner(w, r)
case "/localapi/v0/dial":
h.serveDial(w, r)
case "/localapi/v0/id-token":
h.serveIDToken(w, r)
case "/localapi/v0/upload-client-metrics":
h.serveUploadClientMetrics(w, r)
case "/localapi/v0/tka/status":
h.serveTkaStatus(w, r)
case "/localapi/v0/tka/init":
h.serveTkaInit(w, r)
case "/":
io.WriteString(w, "tailscaled\n")
default:
http.Error(w, "404 not found", 404)
}
if runtime.GOOS == "windows" && port != safesocket.WindowsLocalPort {
return false
}
if host == "localhost" {
return true
}
addr, err := netip.ParseAddr(h)
if err != nil {
return false
}
return addr.IsLoopback()
}
// handlerForPath returns the LocalAPI handler for the provided Request.URI.Path.
// (the path doesn't include any query parameters)
func handlerForPath(urlPath string) (h localAPIHandler, ok bool) {
if urlPath == "/" {
return (*Handler).serveLocalAPIRoot, true
}
suff, ok := strs.CutPrefix(urlPath, "/localapi/v0/")
if !ok {
// Currently all LocalAPI methods start with "/localapi/v0/" to signal
// to people that they're not necessarily stable APIs. In practice we'll
// probably need to keep them pretty stable anyway, but for now treat
// them as an internal implementation detail.
return nil, false
}
if fn, ok := handler[suff]; ok {
// Here we match exact handler suffixes like "status" or ones with a
// slash already in their name, like "tka/status".
return fn, true
}
// Otherwise, it might be a prefix match like "files/*" which we look up
// by the prefix including first trailing slash.
if i := strings.IndexByte(suff, '/'); i != -1 {
suff = suff[:i+1]
if fn, ok := handler[suff]; ok {
return fn, true
}
}
return nil, false
}
func (*Handler) serveLocalAPIRoot(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "tailscaled\n")
}
// serveIDToken handles requests to get an OIDC ID token.
@@ -268,86 +212,20 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bugreport access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
return
}
logMarker := func() string {
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
}
logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
if envknob.NoLogsNoSupport() {
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
logMarker = "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled"
}
startMarker := logMarker()
h.logf("user bugreport: %s", startMarker)
if note := r.URL.Query().Get("note"); len(note) > 0 {
h.logf("user bugreport: %s", logMarker)
if note := r.FormValue("note"); len(note) > 0 {
h.logf("user bugreport note: %s", note)
}
hi, _ := json.Marshal(hostinfo.New())
h.logf("user bugreport hostinfo: %s", hi)
if err := health.OverallError(); err != nil {
h.logf("user bugreport health: %s", err.Error())
} else {
h.logf("user bugreport health: ok")
}
if defBool(r.URL.Query().Get("diagnose"), false) {
if defBool(r.FormValue("diagnose"), false) {
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, startMarker)
// Nothing else to do if we're not in record mode; we wrote the marker
// above, so we can just finish our response now.
if !defBool(r.URL.Query().Get("record"), false) {
return
}
until := time.Now().Add(12 * time.Hour)
var changed map[string]bool
for _, component := range []string{"magicsock"} {
if h.b.GetComponentDebugLogging(component).IsZero() {
if err := h.b.SetComponentDebugLogging(component, until); err != nil {
h.logf("bugreport: error setting component %q logging: %v", component, err)
continue
}
mak.Set(&changed, component, true)
}
}
defer func() {
for component := range changed {
h.b.SetComponentDebugLogging(component, time.Time{})
}
}()
// NOTE(andrew): if we have anything else we want to do while recording
// a bugreport, we can add it here.
// Read from the client; this will also return when the client closes
// the connection.
var buf [1]byte
_, err := r.Body.Read(buf[:])
switch {
case err == nil:
// good
case errors.Is(err, io.EOF):
// good
case errors.Is(err, io.ErrUnexpectedEOF):
// this happens when Ctrl-C'ing the tailscale client; don't
// bother logging an error
default:
// Log but continue anyway.
h.logf("user bugreport: error reading body: %v", err)
}
// Generate another log marker and return it to the client.
endMarker := logMarker()
h.logf("user bugreport end: %s", endMarker)
fmt.Fprintln(w, endMarker)
fmt.Fprintln(w, logMarker)
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
@@ -440,24 +318,6 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "done\n")
}
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs"))
err := h.b.SetComponentDebugLogging(component, time.Now().Add(time.Duration(secs)*time.Second))
var res struct {
Error string
}
if err != nil {
res.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
// for platforms where we want to link it in.
var serveProfileFunc func(http.ResponseWriter, *http.Request)
@@ -546,7 +406,7 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
http.Error(w, "prefs access denied", http.StatusForbidden)
return
}
var prefs ipn.PrefsView
var prefs *ipn.Prefs
switch r.Method {
case "PATCH":
if !h.PermitWrite {
@@ -645,7 +505,6 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
}
defer rc.Close()
w.Header().Set("Content-Length", fmt.Sprint(size))
w.Header().Set("Content-Type", "application/octet-stream")
io.Copy(w, rc)
}
@@ -944,13 +803,13 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
json.NewEncoder(w).Encode(struct{}{})
}
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "lock status access denied", http.StatusForbidden)
return
}
if r.Method != http.MethodGet {
http.Error(w, "use GET", http.StatusMethodNotAllowed)
http.Error(w, "use Get", http.StatusMethodNotAllowed)
return
}
@@ -963,7 +822,7 @@ func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
w.Write(j)
}
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "lock init access denied", http.StatusForbidden)
return
@@ -996,40 +855,6 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
w.Write(j)
}
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
var req modifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", 400)
return
}
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
return
}
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}
func defBool(a string, def bool) bool {
if a == "" {
return def

View File

@@ -6,8 +6,12 @@ package ipn
import (
"bytes"
"context"
"encoding/json"
"testing"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
)
@@ -57,6 +61,133 @@ func TestReadWrite(t *testing.T) {
}
}
func TestClientServer(t *testing.T) {
tstest.PanicOnLog()
tstest.ResourceCheck(t)
b := &FakeBackend{}
var bs *BackendServer
var bc *BackendClient
serverToClientCh := make(chan []byte, 16)
defer close(serverToClientCh)
go func() {
for b := range serverToClientCh {
bc.GotNotifyMsg(b)
}
}()
serverToClient := func(n Notify) {
b, err := json.Marshal(n)
if err != nil {
panic(err.Error())
}
serverToClientCh <- append([]byte{}, b...)
}
clientToServer := func(b []byte) {
bs.GotCommandMsg(context.TODO(), b)
}
slogf := func(fmt string, args ...any) {
t.Logf("s: "+fmt, args...)
}
clogf := func(fmt string, args ...any) {
t.Logf("c: "+fmt, args...)
}
bs = NewBackendServer(slogf, b, serverToClient)
// Verify that this doesn't break bs's callback:
NewBackendServer(slogf, b, nil)
bc = NewBackendClient(clogf, clientToServer)
ch := make(chan Notify, 256)
notify := func(n Notify) { ch <- n }
h, err := NewHandle(bc, clogf, notify, Options{
Prefs: &Prefs{
ControlURL: "http://example.com/fake",
},
})
if err != nil {
t.Fatalf("NewHandle error: %v\n", err)
}
notes := Notify{}
nn := []Notify{}
processNote := func(n Notify) {
nn = append(nn, n)
if n.State != nil {
t.Logf("state change: %v", *n.State)
notes.State = n.State
}
if n.Prefs != nil {
notes.Prefs = n.Prefs
}
if n.NetMap != nil {
notes.NetMap = n.NetMap
}
if n.Engine != nil {
notes.Engine = n.Engine
}
if n.BrowseToURL != nil {
notes.BrowseToURL = n.BrowseToURL
}
}
notesState := func() State {
if notes.State != nil {
return *notes.State
}
return NoState
}
flushUntil := func(wantFlush State) {
t.Helper()
timer := time.NewTimer(1 * time.Second)
loop:
for {
select {
case n := <-ch:
processNote(n)
if notesState() == wantFlush {
break loop
}
case <-timer.C:
t.Fatalf("timeout waiting for state %v, got %v", wantFlush, notes.State)
}
}
timer.Stop()
loop2:
for {
select {
case n := <-ch:
processNote(n)
default:
break loop2
}
}
if got, want := h.State(), notesState(); got != want {
t.Errorf("h.State()=%v, notes.State=%v (on flush until %v)\n", got, want, wantFlush)
}
}
flushUntil(NeedsLogin)
h.StartLoginInteractive()
flushUntil(Running)
if notes.NetMap == nil && h.NetMap() != nil {
t.Errorf("notes.NetMap == nil while h.NetMap != nil\nnotes:\n%v", nn)
}
h.UpdatePrefs(func(p *Prefs) {
p.WantRunning = false
})
flushUntil(Stopped)
h.Logout()
flushUntil(NeedsLogin)
h.Login(&tailcfg.Oauth2Token{
AccessToken: "google_id_token",
TokenType: GoogleIDTokenType,
})
flushUntil(Running)
}
func TestNilBackend(t *testing.T) {
var called *Notify
bs := NewBackendServer(t.Logf, nil, func(n Notify) {

View File

@@ -27,7 +27,7 @@ import (
"tailscale.com/util/dnsname"
)
//go:generate go run tailscale.com/cmd/viewer -type=Prefs
//go:generate go run tailscale.com/cmd/cloner -type=Prefs
// DefaultControlURL is the URL base of the control plane
// ("coordination server") for use when no explicit one is configured.
@@ -288,8 +288,6 @@ func (m *MaskedPrefs) Pretty() string {
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
func (p PrefsView) Pretty() string { return p.ж.Pretty() }
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
func (p *Prefs) pretty(goos string) string {
var sb strings.Builder
@@ -349,10 +347,6 @@ func (p *Prefs) pretty(goos string) string {
return sb.String()
}
func (p PrefsView) ToBytes() []byte {
return p.ж.ToBytes()
}
func (p *Prefs) ToBytes() []byte {
data, err := json.MarshalIndent(p, "", "\t")
if err != nil {
@@ -361,10 +355,6 @@ func (p *Prefs) ToBytes() []byte {
return data
}
func (p PrefsView) Equals(p2 PrefsView) bool {
return p.ж.Equals(p2.ж)
}
func (p *Prefs) Equals(p2 *Prefs) bool {
if p == nil && p2 == nil {
return true
@@ -441,14 +431,6 @@ func NewPrefs() *Prefs {
}
}
// ControlURLOrDefault returns the coordination server's URL base.
//
// If not configured, or if the configured value is a legacy name equivalent to
// the default, then DefaultControlURL is returned instead.
func (p PrefsView) ControlURLOrDefault() string {
return p.ж.ControlURLOrDefault()
}
// ControlURLOrDefault returns the coordination server's URL base.
//
// If not configured, or if the configured value is a legacy name equivalent to
@@ -606,12 +588,6 @@ func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
return err
}
// ShouldSSHBeRunning reports whether the SSH server should be running based on
// the prefs.
func (p PrefsView) ShouldSSHBeRunning() bool {
return p.Valid() && p.ж.ShouldSSHBeRunning()
}
// ShouldSSHBeRunning reports whether the SSH server should be running based on
// the prefs.
func (p *Prefs) ShouldSSHBeRunning() bool {

View File

@@ -470,7 +470,7 @@ func TestLoadPrefsNotExist(t *testing.T) {
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
}
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs handles corrupted input files.
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
// See issue #954 for details.
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
f, err := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")
@@ -826,22 +826,3 @@ func TestControlURLOrDefault(t *testing.T) {
t.Errorf("got %q; want %q", got, want)
}
}
func TestNotifyPrefsJSONRoundtrip(t *testing.T) {
var n Notify
if n.Prefs != nil && n.Prefs.Valid() {
t.Fatal("Prefs should not be valid at start")
}
b, err := json.Marshal(n)
if err != nil {
t.Fatal(err)
}
var n2 Notify
if err := json.Unmarshal(b, &n2); err != nil {
t.Fatal(err)
}
if n2.Prefs != nil && n2.Prefs.Valid() {
t.Fatal("Prefs should not be valid after deserialization")
}
}

View File

@@ -6,8 +6,6 @@ package ipn
import (
"errors"
"fmt"
"strconv"
)
// ErrStateNotExist is returned by StateStore.ReadState when the
@@ -37,7 +35,7 @@ const (
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
// NLKeyStateKey is the key under which we store the node's
// NLKeyStateKey is the key under which we store the nodes'
// network-lock node key, in its key.NLPrivate.MarshalText representation.
NLKeyStateKey = StateKey("_nl-node-key")
)
@@ -50,17 +48,3 @@ type StateStore interface {
// WriteState saves bs as the state associated with ID.
WriteState(id StateKey, bs []byte) error
}
// ReadStoreInt reads an integer from a StateStore.
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
v, err := store.ReadState(id)
if err != nil {
return 0, err
}
return strconv.ParseInt(string(v), 10, 64)
}
// PutStoreInt puts an integer into a StateStore.
func PutStoreInt(store StateStore, id StateKey, val int64) error {
return store.WriteState(id, fmt.Appendf(nil, "%d", val))
}

View File

@@ -100,9 +100,7 @@ func (c *Client) secretURL(name string) string {
}
func getError(resp *http.Response) error {
if resp.StatusCode == 200 || resp.StatusCode == 201 {
// These are the only success codes returned by the Kubernetes API.
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#http-status-codes
if resp.StatusCode == 200 {
return nil
}
st := &Status{}

View File

@@ -38,7 +38,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/777337dba4cf/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
@@ -55,9 +55,9 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/a9213eeb:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/807a2327:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/a66eb644:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/f0f3c7e8:LICENSE))

View File

@@ -27,7 +27,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/777337dba4cf/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
@@ -39,9 +39,9 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/a9213eeb:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/886fb937:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/f0f3c7e8:LICENSE))
- [golang.zx2c4.com/wireguard](https://pkg.go.dev/golang.zx2c4.com/wireguard) ([MIT](https://git.zx2c4.com/wireguard-go/tree/LICENSE?id=b51010ba13f0))

View File

@@ -58,7 +58,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.4/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/62f465106986/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
@@ -71,9 +71,9 @@ Some packages may only be included on certain architectures or operating systems
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/eb4f295c:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/a9213eeb:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/f0f3c7e8:LICENSE))

View File

@@ -31,9 +31,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/886fb937:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))

View File

@@ -70,24 +70,11 @@ func getLogTarget() string {
return getLogTargetOnce.v
}
// LogURL is the base URL for the configured logtail server, or the default.
// It is guaranteed to not terminate with any forward slashes.
func LogURL() string {
if v := getLogTarget(); v != "" {
return strings.TrimRight(v, "/")
}
return "https://" + logtail.DefaultHost
}
// LogHost returns the hostname only (without port) of the configured
// logtail server, or the default.
//
// Deprecated: Use LogURL instead.
func LogHost() string {
if v := getLogTarget(); v != "" {
if u, err := url.Parse(v); err == nil {
return u.Hostname()
}
return v
}
return logtail.DefaultHost
}
@@ -609,7 +596,7 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy {
}
}
log.SetFlags(0) // other log flags are set on console, not here
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(logOutput)
log.Printf("Program starting: v%v, Go %v: %#v",

View File

@@ -1,37 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logpolicy
import (
"os"
"reflect"
"testing"
)
func TestLogHost(t *testing.T) {
v := reflect.ValueOf(&getLogTargetOnce).Elem()
reset := func() {
v.Set(reflect.Zero(v.Type()))
}
defer reset()
tests := []struct {
env string
want string
}{
{"", "log.tailscale.io"},
{"http://foo.com", "foo.com"},
{"https://foo.com", "foo.com"},
{"https://foo.com/", "foo.com"},
{"https://foo.com:123/", "foo.com"},
}
for _, tt := range tests {
reset()
os.Setenv("TS_LOG_TARGET", tt.env)
if got := LogHost(); got != tt.want {
t.Errorf("for env %q, got %q, want %q", tt.env, got, tt.want)
}
}
}

View File

@@ -34,7 +34,7 @@ func NewPrivateID() (id PrivateID, err error) {
func (id PrivateID) MarshalText() ([]byte, error) {
b := make([]byte, hex.EncodedLen(len(id)))
if i := hex.Encode(b, id[:]); i != len(b) {
return nil, fmt.Errorf("logtail.PrivateID.MarshalText: i=%d", i)
return nil, fmt.Errorf("logtail.PrivateID.MarhsalText: i=%d", i)
}
return b, nil
}
@@ -122,7 +122,7 @@ func MustParsePublicID(s string) PublicID {
func (id PublicID) MarshalText() ([]byte, error) {
b := make([]byte, hex.EncodedLen(len(id)))
if i := hex.Encode(b, id[:]); i != len(b) {
return nil, fmt.Errorf("logtail.PublicID.MarshalText: i=%d", i)
return nil, fmt.Errorf("logtail.PublicID.MarhsalText: i=%d", i)
}
return b, nil
}

View File

@@ -44,13 +44,12 @@ type Encoder interface {
type Config struct {
Collection string // collection name, a domain name
PrivateID PrivateID // private ID for the primary log stream
CopyPrivateID PrivateID // private ID for a log stream that is a superset of this log stream
PrivateID PrivateID // machine-specific private identifier
BaseURL string // if empty defaults to "https://log.tailscale.io"
HTTPC *http.Client // if empty defaults to http.DefaultClient
SkipClientTime bool // if true, client_time is not written to logs
LowMemory bool // if true, logtail minimizes memory use
TimeNow func() time.Time // if set, substitutes uses of time.Now
TimeNow func() time.Time // if set, subsitutes uses of time.Now
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
Buffer Buffer // temp storage, if nil a MemoryBuffer
@@ -74,7 +73,7 @@ type Config struct {
// IncludeProcSequence, if true, results in an ephemeral sequence number
// being included in the logs. The sequence number is incremented for each
// log message sent, but is not persisted across process restarts.
// log message sent, but is not peristed across process restarts.
IncludeProcSequence bool
}
@@ -113,16 +112,12 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
stdLogf := func(f string, a ...any) {
fmt.Fprintf(cfg.Stderr, strings.TrimSuffix(f, "\n")+"\n", a...)
}
var urlSuffix string
if !cfg.CopyPrivateID.IsZero() {
urlSuffix = "?copyId=" + cfg.CopyPrivateID.String()
}
l := &Logger{
privateID: cfg.PrivateID,
stderr: cfg.Stderr,
stderrLevel: int64(cfg.StderrLevel),
httpc: cfg.HTTPC,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String() + urlSuffix,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
lowMem: cfg.LowMemory,
buffer: cfg.Buffer,
skipClientTime: cfg.SkipClientTime,
@@ -524,7 +519,7 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, proc
b = append(b, `"logtail": {`...)
if !skipClientTime {
b = append(b, `"client_time": "`...)
b = now.UTC().AppendFormat(b, time.RFC3339Nano)
b = now.AppendFormat(b, time.RFC3339Nano)
b = append(b, `",`...)
}
if procID != 0 {
@@ -617,7 +612,7 @@ func (l *Logger) encodeLocked(buf []byte, level int) []byte {
if !l.skipClientTime || l.procID != 0 || l.procSequence != 0 {
logtail := map[string]any{}
if !l.skipClientTime {
logtail["client_time"] = now.UTC().Format(time.RFC3339Nano)
logtail["client_time"] = now.Format(time.RFC3339Nano)
}
if l.procID != 0 {
logtail["proc_id"] = l.procID

View File

@@ -381,7 +381,7 @@ func (m *Manager) NextPacket() ([]byte, error) {
return buf, nil
}
// Query executes a DNS query received from the given address. The query is
// Query executes a DNS query recieved from the given address. The query is
// provided in bs as a wire-encoded DNS query without any transport header.
// This method is called for requests arriving over UDP and TCP.
func (m *Manager) Query(ctx context.Context, bs []byte, from netip.AddrPort) ([]byte, error) {
@@ -540,7 +540,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
logf("creating dns cleanup: %v", err)
return
}
dns := NewManager(logf, oscfg, nil, &tsdial.Dialer{Logf: logf}, nil)
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}

View File

@@ -274,7 +274,7 @@ func runTest(t *testing.T, isLocal bool) {
runCase := func(n int) {
t.Logf("Test case: %d domains\n", n)
if !isLocal {
// When !isLocal, we want to check that a GP notification occurred for
// When !isLocal, we want to check that a GP notification occured for
// every single test case.
trk, err = newGPNotificationTracker()
if err != nil {

View File

@@ -302,7 +302,7 @@ func (m *nmManager) GetBaseConfig() (OSConfig, error) {
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.
// Config for the taislcale interface, skip.
continue
}
}

View File

@@ -58,7 +58,7 @@ var (
const _RP_FORCE = 1 // Flag for RefreshPolicyEx
// nrptRuleDatabase encapsulates access to the Windows Name Resolution Policy
// nrptRuleDatabase ensapsulates access to the Windows Name Resolution Policy
// Table (NRPT).
type nrptRuleDatabase struct {
logf logger.Logf

View File

@@ -37,15 +37,15 @@ func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
}
// NextDNS DoH URLs are of the form "https://dns.nextdns.io/c3a884"
// where the path component is the lower 12 bytes of the IPv6 address
// where the path component is the lower 8 bytes of the IPv6 address
// in lowercase hex without any zero padding.
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
a := ip.As16()
var sb strings.Builder
const base = "https://dns.nextdns.io/"
sb.Grow(len(base) + 12)
sb.Grow(len(base) + 8)
sb.WriteString(base)
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
for _, b := range bytes.TrimLeft(a[8:], "\x00") {
fmt.Fprintf(&sb, "%02x", b)
}
return sb.String(), true, true
@@ -100,7 +100,7 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
// conventional for them and not required (it'll already be in the DoH path).
// (Really we shouldn't use either IPv4 or IPv6 anycast for DoH once we
// resolve "dns.nextdns.io".)
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 12 && len(b) > 0 {
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 8 && len(b) > 0 {
return []netip.Addr{
nextDNSv4One,
nextDNSv4Two,
@@ -215,7 +215,7 @@ var (
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
// provided ip and using id as the lowest 0-8 bytes.
func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
if len(id) > 12 {
if len(id) > 8 {
return netip.Addr{}
}
a := ip.As16()

View File

@@ -86,19 +86,6 @@ func TestDoHIPsOfBase(t *testing.T) {
"2a07:a8c1::c3:a884",
),
},
{
base: "https://dns.nextdns.io/112233445566778899aabbcc",
want: ips(
"45.90.28.0",
"45.90.30.0",
"2a07:a8c0:1122:3344:5566:7788:99aa:bbcc",
"2a07:a8c1:1122:3344:5566:7788:99aa:bbcc",
),
},
{
base: "https://dns.nextdns.io/112233445566778899aabbccdd",
want: ips(), // nothing; profile length is over 12 bytes
},
{
base: "https://dns.nextdns.io/c3a884/with/more/stuff",
want: ips(

View File

@@ -180,7 +180,7 @@ type resolverAndDelay struct {
type forwarder struct {
logf logger.Logf
linkMon *monitor.Mon
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absorbs it
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
dialer *tsdial.Dialer
dohSem chan struct{}
@@ -502,7 +502,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
// Only known DoH providers are supported currently. Specifically, we
// only support DoH providers where we can TCP connect to them on port
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custom DoH providers
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custon DoH providers
// aren't currently supported. There's no backup DNS resolution path for
// them.
urlBase := rr.name.Addr

View File

@@ -609,7 +609,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
metricDNSResolveLocalOKAll.Add(1)
return addrs[0], dns.RCodeSuccess
// Leave some record types explicitly unimplemented.
// Leave some some record types explicitly unimplemented.
// These types relate to recursive resolution or special
// DNS semantics and might be implemented in the future.
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:

View File

@@ -276,11 +276,6 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Add
return netip.Addr{}, netip.Addr{}, nil, fmt.Errorf("no IPs for %q found", host)
}
// Unmap everything; LookupNetIP can return mapped addresses (see #5698)
for i := range ips {
ips[i] = ips[i].Unmap()
}
have4 := false
for _, ipa := range ips {
if ipa.Is4() {

View File

@@ -99,7 +99,7 @@ type msgResource struct {
}
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
// when the request can not be satisfied from cache.
// when the request can not be satisified from cache.
var ErrCacheMiss = errors.New("cache miss")
var parserPool = &sync.Pool{
@@ -264,7 +264,7 @@ func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
}
// packDNSResponse builds a DNS response for the given question and
// transaction ID. The response resource records will have the
// transaction ID. The response resource records will have have the
// same provided TTL.
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
var baseMem []byte // TODO: guess a max size based on looping over answers?

View File

@@ -20,9 +20,9 @@ import (
// Tuple is a 5-tuple of proto, source and destination IP and port.
type Tuple struct {
Proto ipproto.Proto `json:"proto"`
Src netip.AddrPort `json:"src"`
Dst netip.AddrPort `json:"dst"`
Proto ipproto.Proto
Src netip.AddrPort
Dst netip.AddrPort
}
func (t Tuple) String() string {

View File

@@ -441,13 +441,13 @@ func prefixesEqual(a, b []netip.Prefix) bool {
// UseInterestingInterfaces is an InterfaceFilter that reports whether i is an interesting interface.
// An interesting interface if it is (a) not owned by Tailscale and (b) routes interesting IP addresses.
// See UseInterestingIPs for the definition of an interesting IP address.
// See UseInterestingIPs for the defition of an interesting IP address.
func UseInterestingInterfaces(i Interface, ips []netip.Prefix) bool {
return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips)
}
// UseInterestingIPs is an IPFilter that reports whether ip is an interesting IP address.
// An IP address is interesting if it is neither a loopback nor a link local unicast IP address.
// An IP address is interesting if it is neither a lopback not a link local unicast IP address.
func UseInterestingIPs(ip netip.Addr) bool {
return isInterestingIP(ip)
}
@@ -455,7 +455,7 @@ func UseInterestingIPs(ip netip.Addr) bool {
// UseAllInterfaces is an InterfaceFilter that includes all interfaces.
func UseAllInterfaces(i Interface, ips []netip.Prefix) bool { return true }
// UseAllIPs is an IPFilter that includes all IPs.
// UseAllIPs is an IPFilter that includes all all IPs.
func UseAllIPs(ips netip.Addr) bool { return true }
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }

View File

@@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Common code for FreeBSD and Darwin. This might also work on other
// BSD systems (e.g. OpenBSD) but has not been tested.
// This might work on other BSDs, but only tested on FreeBSD.
// Originally a fork of interfaces_darwin.go with slightly different flags.
//go:build darwin || freebsd
// +build darwin freebsd
//go:build freebsd
// +build freebsd
package interfaces
@@ -37,6 +37,11 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
return d, nil
}
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
}
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
@@ -56,20 +61,35 @@ func DefaultRouteInterfaceIndex() (int, error) {
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := parseRoutingTable(rib)
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
if isDefaultGateway(rm) {
return rm.Index, nil
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
}
}
return 0, errors.New("no gateway index found")
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
}
func init() {
@@ -82,7 +102,7 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
log.Printf("routerIP/FetchRIB: %v", err)
return ret, false
}
msgs, err := parseRoutingTable(rib)
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
if err != nil {
log.Printf("routerIP/ParseRIB: %v", err)
return ret, false
@@ -92,59 +112,26 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
if !ok {
continue
}
if !isDefaultGateway(rm) {
const RTF_IFSCOPE = 0x1000000
if rm.Flags&unix.RTF_GATEWAY == 0 {
continue
}
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
if len(rm.Addrs) > unix.RTAX_GATEWAY {
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
// Expect 0.0.0.0 as DST field.
continue
}
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
continue
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
}
}
return ret, false
}
var v4default = [4]byte{0, 0, 0, 0}
var v6default = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
func isDefaultGateway(rm *route.RouteMessage) bool {
if rm.Flags&unix.RTF_GATEWAY == 0 {
return false
}
// Defined locally because FreeBSD does not have unix.RTF_IFSCOPE.
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_IFSCOPE != 0 {
return false
}
// Addrs is [RTAX_DST, RTAX_GATEWAY, RTAX_NETMASK, ...]
if len(rm.Addrs) <= unix.RTAX_NETMASK {
return false
}
dst := rm.Addrs[unix.RTAX_DST]
netmask := rm.Addrs[unix.RTAX_NETMASK]
if dst == nil || netmask == nil {
return false
}
if dst.Family() == syscall.AF_INET && netmask.Family() == syscall.AF_INET {
dstAddr, dstOk := dst.(*route.Inet4Addr)
nmAddr, nmOk := netmask.(*route.Inet4Addr)
if dstOk && nmOk && dstAddr.IP == v4default && nmAddr.IP == v4default {
return true
}
}
if dst.Family() == syscall.AF_INET6 && netmask.Family() == syscall.AF_INET6 {
dstAddr, dstOk := dst.(*route.Inet6Addr)
nmAddr, nmOk := netmask.(*route.Inet6Addr)
if dstOk && nmOk && dstAddr.IP == v6default && nmAddr.IP == v6default {
return true
}
}
return false
}

View File

@@ -5,16 +5,128 @@
package interfaces
import (
"errors"
"fmt"
"log"
"net"
"net/netip"
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
)
func defaultRoute() (d DefaultRouteDetails, err error) {
idx, err := DefaultRouteInterfaceIndex()
if err != nil {
return d, err
}
iface, err := net.InterfaceByIndex(idx)
if err != nil {
return d, err
}
d.InterfaceName = iface.Name
d.InterfaceIndex = idx
return d, nil
}
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
}
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
// Internet:
// Destination Gateway Flags Netif Expire
// default 10.0.0.1 UGSc en0 <-- want this one
// default 10.0.0.1 UGScI en1
// From man netstat:
// U RTF_UP Route usable
// G RTF_GATEWAY Destination requires forwarding by intermediary
// S RTF_STATIC Manually added
// c RTF_PRCLONING Protocol-specified generate new routes on use
// I RTF_IFSCOPE Route is associated with an interface scope
rib, err := fetchRoutingTable()
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
}
}
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
}
func init() {
likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB
}
func likelyHomeRouterIPDarwinFetchRIB() (ret netip.Addr, ok bool) {
rib, err := fetchRoutingTable()
if err != nil {
log.Printf("routerIP/FetchRIB: %v", err)
return ret, false
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
log.Printf("routerIP/ParseRIB: %v", err)
return ret, false
}
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
if len(rm.Addrs) > unix.RTAX_GATEWAY {
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
// Expect 0.0.0.0 as DST field.
continue
}
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
continue
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
}
}
return ret, false
}

View File

@@ -16,32 +16,18 @@ import (
)
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
syscallIP, syscallOK := likelyHomeRouterIPBSDFetchRIB()
netstatIP, netstatIf, netstatOK := likelyHomeRouterIPDarwinExec()
syscallIP, syscallOK := likelyHomeRouterIPDarwinFetchRIB()
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
if syscallOK != netstatOK || syscallIP != netstatIP {
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
syscallIP, syscallOK,
netstatIP, netstatOK,
)
}
if !syscallOK {
return
}
def, err := defaultRoute()
if err != nil {
t.Errorf("defaultRoute() error: %v", err)
}
if def.InterfaceName != netstatIf {
t.Errorf("syscall default route interface %s differs from netstat %s", def.InterfaceName, netstatIf)
}
}
/*
Parse out 10.0.0.1 and en0 from:
Parse out 10.0.0.1 from:
$ netstat -r -n -f inet
Routing tables
@@ -54,12 +40,12 @@ default link#14 UCSI utun2
10.0.0.1/32 link#4 UCS en0 !
...
*/
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
if version.IsMobile() {
// Don't try to do subprocesses on iOS. Ends up with log spam like:
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
// This is why we have likelyHomeRouterIPDarwinSyscall.
return ret, "", false
return ret, false
}
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
stdout, err := cmd.StdoutPipe()
@@ -78,26 +64,22 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
return nil
}
f = mem.AppendFields(f[:0], line)
if len(f) < 4 || !f[0].EqualString("default") {
if len(f) < 3 || !f[0].EqualString("default") {
return nil
}
ipm, flagsm, netifm := f[1], f[2], f[3]
ipm, flagsm := f[1], f[2]
if !mem.Contains(flagsm, mem.S("G")) {
return nil
}
if mem.Contains(flagsm, mem.S("I")) {
return nil
}
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
if err == nil && ip.IsPrivate() {
ret = ip
netif = netifm.StringCopy()
// We've found what we're looking for.
return errStopReadingNetstatTable
}
return nil
})
return ret, netif, ret.IsValid()
return ret, ret.IsValid()
}
func TestFetchRoutingTable(t *testing.T) {

View File

@@ -1,26 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This might work on other BSDs, but only tested on FreeBSD.
//go:build freebsd
// +build freebsd
package interfaces
import (
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
}
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
}

View File

@@ -19,7 +19,6 @@ import (
"net/netip"
"runtime"
"sort"
"strings"
"sync"
"time"
@@ -162,7 +161,7 @@ type Client struct {
// GetSTUNConn4 optionally provides a func to return the
// connection to use for sending & receiving IPv4 packets. If
// nil, an ephemeral one is created as needed.
// nil, an emphemeral one is created as needed.
GetSTUNConn4 func() STUNConn
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
@@ -1106,39 +1105,21 @@ func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, pr
}
rids = append(rids, id)
}
if len(rids) == 0 {
return false, nil
}
preferredDERP = rids[rand.Intn(len(rids))]
}
node := dm.Regions[preferredDERP].Nodes[0]
if strings.HasSuffix(node.HostName, tailcfg.DotInvalid) {
// Don't try to connect to invalid hostnames. This occurred in tests:
// https://github.com/tailscale/tailscale/issues/6207
// TODO(bradfitz,andrew-d): how to actually handle this nicely?
return false, nil
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+node.HostName+"/generate_204", nil)
if err != nil {
return false, err
}
chal := "tailscale " + node.HostName
req.Header.Set("X-Tailscale-Challenge", chal)
r, err := noRedirectClient.Do(req)
if err != nil {
return false, err
}
defer r.Body.Close()
c.logf("[v2] checkCaptivePortal url=%q status_code=%d", req.URL.String(), r.StatusCode)
expectedResponse := "response " + chal
validResponse := r.Header.Get("X-Tailscale-Response") == expectedResponse
c.logf("[v2] checkCaptivePortal url=%q status_code=%d valid_response=%v", req.URL.String(), r.StatusCode, validResponse)
return r.StatusCode != 204 || !validResponse, nil
return r.StatusCode != 204, nil
}
// runHTTPOnlyChecks is the netcheck done by environments that can

View File

@@ -6,7 +6,6 @@ package nettest
import (
"net"
"net/netip"
"time"
)
@@ -33,38 +32,20 @@ func NewConn(name string, maxBuf int) (Conn, Conn) {
return &connHalf{r: r, w: w}, &connHalf{r: w, w: r}
}
// NewTCPConn creates a pair of Conns that are wired together by pipes.
func NewTCPConn(src, dst netip.AddrPort, maxBuf int) (local Conn, remote Conn) {
r := NewPipe(src.String(), maxBuf)
w := NewPipe(dst.String(), maxBuf)
lAddr := net.TCPAddrFromAddrPort(src)
rAddr := net.TCPAddrFromAddrPort(dst)
return &connHalf{r: r, w: w, remote: rAddr, local: lAddr}, &connHalf{r: w, w: r, remote: lAddr, local: rAddr}
}
type connAddr string
func (a connAddr) Network() string { return "mem" }
func (a connAddr) String() string { return string(a) }
type connHalf struct {
local, remote net.Addr
r, w *Pipe
r, w *Pipe
}
func (c *connHalf) LocalAddr() net.Addr {
if c.local != nil {
return c.local
}
return connAddr(c.r.name)
}
func (c *connHalf) RemoteAddr() net.Addr {
if c.remote != nil {
return c.remote
}
return connAddr(c.w.name)
}

View File

@@ -15,7 +15,7 @@ const (
bufferSize = 256 * 1024
)
// Listener is a net.Listener using NewConn to create pairs of network
// Listener is a net.Listener using using NewConn to create pairs of network
// connections connected in memory using a buffered pipe. It also provides a
// Dial method to establish new connections.
type Listener struct {

View File

@@ -195,7 +195,7 @@ const (
// given interface.
// The iface param determines which interface to check against, "" means to check
// global config.
// It tries to lookup the value directly from `/proc/sys`, and falls back to
// It tries to lookup the value directly from `/proc/sys`, and fallsback to
// using `sysctl` on failure.
func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
k := ipForwardSysctlKey(slashFormat, p, iface)

View File

@@ -40,7 +40,7 @@ type Header interface {
}
// HeaderChecksummer is implemented by Header implementations that
// need to do a checksum over their payloads.
// need to do a checksum over their paylods.
type HeaderChecksummer interface {
Header

View File

@@ -202,7 +202,7 @@ func TestPingerMismatch(t *testing.T) {
func mockPinger(t *testing.T, clock *tstest.Clock) (*Pinger, func()) {
// In tests, we use UDP so that we can test without being root; this
// doesn't matter because we mock out the ICMP reply below to be a real
// doesn't matter becuase we mock out the ICMP reply below to be a real
// ICMP echo reply packet.
conn, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {

View File

@@ -19,7 +19,7 @@ import (
"tailscale.com/types/logger"
)
// TestIGD is an IGD (Internet Gateway Device) for testing. It supports fake
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
// implementations of NAT-PMP, PCP, and/or UPnP to test clients against.
type TestIGD struct {
upnpConn net.PacketConn // for UPnP discovery

View File

@@ -122,7 +122,7 @@ func DERPMapOf(stun ...string) *tailcfg.DERPMap {
node := &tailcfg.DERPNode{
Name: fmt.Sprint(regionID) + "a",
RegionID: regionID,
HostName: fmt.Sprintf("d%d%s", regionID, tailcfg.DotInvalid),
HostName: fmt.Sprintf("d%d.invalid", regionID),
IPv4: ipv4,
IPv6: ipv6,
STUNPort: port,

Some files were not shown because too many files have changed in this diff Show More