Compare commits
94 Commits
dsnet/admi
...
v1.14.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3020e58f57 | ||
|
|
f3be05e6ea | ||
|
|
6b5081ab31 | ||
|
|
7afb4a1f43 | ||
|
|
4bc90fee03 | ||
|
|
46e42292a5 | ||
|
|
a5b1456410 | ||
|
|
27d0e7cb0a | ||
|
|
f0b70ff186 | ||
|
|
f5d17dae18 | ||
|
|
ceaecdd4d5 | ||
|
|
8704fb308d | ||
|
|
afb95d7246 | ||
|
|
277bf8f48c | ||
|
|
c995ac72a3 | ||
|
|
e699226e80 | ||
|
|
d8e37edb40 | ||
|
|
0744d75238 | ||
|
|
15835f03b3 | ||
|
|
e78ac523da | ||
|
|
15c87017b8 | ||
|
|
bd911fdb12 | ||
|
|
0111d33eb8 | ||
|
|
62a458f7f4 | ||
|
|
37053801bb | ||
|
|
51976ab3a2 | ||
|
|
246fa67e56 | ||
|
|
6990a314f5 | ||
|
|
3ac731dda1 | ||
|
|
71b375c502 | ||
|
|
0ac2130590 | ||
|
|
c1aa5a2e33 | ||
|
|
f35b8c3ead | ||
|
|
fab296536c | ||
|
|
6731f934a6 | ||
|
|
47045265b9 | ||
|
|
4ff0757d44 | ||
|
|
1dd2552032 | ||
|
|
36ffd509de | ||
|
|
edb338f542 | ||
|
|
faa891c1f2 | ||
|
|
8269a23758 | ||
|
|
bf8556ab86 | ||
|
|
6ef734e493 | ||
|
|
adf696172d | ||
|
|
af30897f0d | ||
|
|
1f006025c2 | ||
|
|
fcca374fa7 | ||
|
|
cd426eaf4c | ||
|
|
9f62cc665e | ||
|
|
5c383bdf5d | ||
|
|
56db3e2548 | ||
|
|
6f8c8c771b | ||
|
|
b7ae529ecc | ||
|
|
e199e407d2 | ||
|
|
d5e1abd0c4 | ||
|
|
57b794c338 | ||
|
|
4c8b5fdec4 | ||
|
|
a666b546fb | ||
|
|
0c038b477f | ||
|
|
278e7de9c9 | ||
|
|
93284209bc | ||
|
|
8ab44b339e | ||
|
|
6da6d47a83 | ||
|
|
a24cee0d67 | ||
|
|
d2aa144dcc | ||
|
|
25e060a841 | ||
|
|
833200da6f | ||
|
|
e804ab29fd | ||
|
|
b2eea1ee00 | ||
|
|
39610aeb09 | ||
|
|
98d557dd24 | ||
|
|
3e7ff5ff98 | ||
|
|
954867fef5 | ||
|
|
c992504375 | ||
|
|
1bca722824 | ||
|
|
00b4c2331b | ||
|
|
9547669787 | ||
|
|
b5a41ff381 | ||
|
|
ec9f3f4cc0 | ||
|
|
c68a12afe9 | ||
|
|
d2d55bd63c | ||
|
|
c6740da624 | ||
|
|
7c7eb8094b | ||
|
|
5aba620fb9 | ||
|
|
b9bd7dbc5d | ||
|
|
26b6fe7f02 | ||
|
|
3700cf9ea4 | ||
|
|
5f45d8f8e6 | ||
|
|
a4e19f2233 | ||
|
|
bdb93c5942 | ||
|
|
26c1183941 | ||
|
|
0796c53404 | ||
|
|
8bdf878832 |
@@ -61,6 +61,6 @@ RUN go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.14
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
5
Makefile
5
Makefile
@@ -18,7 +18,10 @@ buildwindows:
|
||||
build386:
|
||||
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.13.0
|
||||
1.14.4
|
||||
|
||||
45
api.md
45
api.md
@@ -18,6 +18,7 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
@@ -510,6 +511,50 @@ Response:
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-validate-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/validate` - run validation tests against the tailnet's active ACL
|
||||
|
||||
Runs the provided ACL tests against the tailnet's existing ACL. This endpoint does not modify the ACL in any way.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
The POST body should be a JSON formatted array of ACL Tests.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '
|
||||
{
|
||||
[
|
||||
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
If all the tests pass, the response will be empty, with an http status code of 200.
|
||||
|
||||
Failed test error response:
|
||||
A 400 http status code and the errors in the response body.
|
||||
```
|
||||
{
|
||||
"message":"test(s) failed",
|
||||
"data":[
|
||||
{
|
||||
"user":"user1@example.com",
|
||||
"errors":["address \"2.2.2.2:22\": want: Drop, got: Accept"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
@@ -8,6 +8,7 @@ package tailscale
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -16,15 +17,19 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
@@ -91,6 +96,9 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if server := res.Header.Get("Tailscale-Version"); server != version.Long {
|
||||
fmt.Fprintf(os.Stderr, "Warning: client version %q != tailscaled server version %q\n", version.Long, server)
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -293,3 +301,69 @@ func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
}
|
||||
return &derpMap, nil
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// with ?type=pair, the response PEM is first the one private
|
||||
// key PEM block, then the cert PEM blocks.
|
||||
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
||||
if i == -1 {
|
||||
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
||||
}
|
||||
i += len("--\n")
|
||||
keyPEM, certPEM = res[:i], res[i:]
|
||||
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
||||
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// tls.Config.GetCertificate.
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
name := hi.ServerName
|
||||
if !strings.Contains(name, ".") {
|
||||
if v, ok := ExpandSNIName(ctx, name); ok {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
certPEM, keyPEM, err := CertPair(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
st, err := StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
||||
return d, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -1,173 +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.
|
||||
|
||||
// microproxy proxies incoming HTTPS connections to another
|
||||
// destination. Instead of managing its own TLS certificates, it
|
||||
// borrows issued certificates and keys from an autocert directory.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":4430", "server address")
|
||||
certdir = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from")
|
||||
hostname = flag.String("hostname", "", "hostname to serve")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
|
||||
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
|
||||
insecure = flag.Bool("insecure", false, "serve over http, for development")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *logCollection != "" {
|
||||
logpolicy.New(*logCollection)
|
||||
}
|
||||
|
||||
ne, err := url.Parse(*nodeExporter)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *nodeExporter, err)
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(ne)
|
||||
proxy.FlushInterval = time.Second
|
||||
|
||||
if _, err = url.Parse(*goVarsURL); err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux) // registers /debug/*
|
||||
mux.Handle("/metrics", tsweb.Protected(proxy))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
|
||||
Quiet200s: true,
|
||||
Logf: log.Printf,
|
||||
})))
|
||||
|
||||
ch := &certHolder{
|
||||
hostname: *hostname,
|
||||
path: filepath.Join(*certdir, *hostname),
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
if !*insecure {
|
||||
httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = httpsrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type goVarsHandler struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
for k, i := range obj {
|
||||
if prefix != "" {
|
||||
k = prefix + "_" + k
|
||||
}
|
||||
switch v := i.(type) {
|
||||
case map[string]interface{}:
|
||||
promPrint(w, k, v)
|
||||
case float64:
|
||||
const saveConfigReject = "control_save_config_rejected_"
|
||||
const saveConfig = "control_save_config_"
|
||||
switch {
|
||||
case strings.HasPrefix(k, saveConfigReject):
|
||||
fmt.Fprintf(w, "control_save_config_rejected{reason=%q} %f\n", k[len(saveConfigReject):], v)
|
||||
case strings.HasPrefix(k, saveConfig):
|
||||
fmt.Fprintf(w, "control_save_config{reason=%q} %f\n", k[len(saveConfig):], v)
|
||||
default:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *goVarsHandler) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
|
||||
resp, err := http.Get(h.url)
|
||||
if err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var mon map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mon); err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
promPrint(w, "", mon)
|
||||
return nil
|
||||
}
|
||||
|
||||
// certHolder loads and caches a TLS certificate from disk, reloading
|
||||
// it every hour.
|
||||
type certHolder struct {
|
||||
hostname string // only hostname allowed in SNI
|
||||
path string // path of certificate+key combined PEM file
|
||||
|
||||
mu sync.Mutex
|
||||
cert *tls.Certificate // cached parsed cert+key
|
||||
loaded time.Time
|
||||
}
|
||||
|
||||
func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if ch.ServerName != c.hostname {
|
||||
return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if time.Since(c.loaded) > time.Hour {
|
||||
if err := c.loadLocked(); err != nil {
|
||||
log.Printf("Reloading cert %q: %v", c.path, err)
|
||||
// continue anyway, we might be able to serve off the stale cert.
|
||||
}
|
||||
}
|
||||
return c.cert, nil
|
||||
}
|
||||
|
||||
// load reloads the TLS certificate and key from disk. Caller must
|
||||
// hold mu.
|
||||
func (c *certHolder) loadLocked() error {
|
||||
bs, err := ioutil.ReadFile(c.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q: %v", c.path, err)
|
||||
}
|
||||
cert, err := tls.X509KeyPair(bs, bs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing %q: %v", c.path, err)
|
||||
}
|
||||
|
||||
c.cert = &cert
|
||||
c.loaded = time.Now()
|
||||
return nil
|
||||
}
|
||||
107
cmd/tailscale/cli/cert.go
Normal file
107
cmd/tailscale/cli/cert.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2021 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 cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var certCmd = &ffcli.Command{
|
||||
Name: "cert",
|
||||
Exec: runCert,
|
||||
ShortHelp: "get TLS certs",
|
||||
ShortUsage: "cert [flags] <domain>",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("cert", flag.ExitOnError)
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file; defaults to DOMAIN.crt")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file; defaults to DOMAIN.key")
|
||||
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var certArgs struct {
|
||||
certFile string
|
||||
keyFile string
|
||||
serve bool
|
||||
}
|
||||
|
||||
func runCert(ctx context.Context, args []string) error {
|
||||
if certArgs.serve {
|
||||
s := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
|
||||
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
|
||||
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
|
||||
}),
|
||||
}
|
||||
log.Printf("running TLS server on :443 ...")
|
||||
return s.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("Usage: tailscale cert [flags] <domain>")
|
||||
}
|
||||
domain := args[0]
|
||||
|
||||
if certArgs.certFile == "" {
|
||||
certArgs.certFile = domain + ".crt"
|
||||
}
|
||||
if certArgs.keyFile == "" {
|
||||
certArgs.keyFile = domain + ".key"
|
||||
}
|
||||
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certChanged {
|
||||
fmt.Printf("Wrote public cert to %v\n", certArgs.certFile)
|
||||
} else {
|
||||
fmt.Printf("Public cert unchanged at %v\n", certArgs.certFile)
|
||||
}
|
||||
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if keyChanged {
|
||||
fmt.Printf("Wrote private key to %v\n", certArgs.keyFile)
|
||||
} else {
|
||||
fmt.Printf("Private key unchanged at %v\n", certArgs.keyFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
|
||||
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
|
||||
return false, nil
|
||||
}
|
||||
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -107,6 +107,7 @@ change in the future.
|
||||
webCmd,
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
|
||||
@@ -478,6 +478,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// This whole 'up' mechanism is too complicated and results in
|
||||
// hairy stuff like this select. We're ultimately waiting for
|
||||
// 'startingOrRunning' to be done, but even in the case where
|
||||
// it succeeds, other parts may shut down concurrently so we
|
||||
// need to prioritize reads from 'startingOrRunning' if it's
|
||||
// readable; its send does happen before the pump mechanism
|
||||
// shuts down. (Issue 2333)
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
@@ -489,6 +496,11 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
@@ -20,8 +20,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
@@ -44,9 +43,9 @@ 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/paths from tailscale.com/cmd/tailscale/cli+
|
||||
💣 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/interfaces+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
@@ -101,9 +100,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from net/http
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -126,10 +124,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+
|
||||
debug/dwarf from debug/elf+
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
@@ -143,8 +137,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
@@ -171,10 +164,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember
|
||||
path from debug/dwarf+
|
||||
path from html/template+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
sort from compress/flate+
|
||||
|
||||
@@ -25,6 +25,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
@@ -87,7 +88,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
@@ -128,12 +128,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
@@ -158,7 +158,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
@@ -166,7 +166,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
@@ -178,6 +178,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
@@ -216,9 +217,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -242,10 +242,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/net/dns+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
@@ -259,8 +255,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
@@ -288,7 +283,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from debug/dwarf+
|
||||
path from github.com/godbus/dbus/v5+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
|
||||
@@ -163,6 +163,34 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func trySynologyMigration(p string) error {
|
||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||
return nil
|
||||
}
|
||||
|
||||
fi, err := os.Stat(p)
|
||||
if err == nil && fi.Size() > 0 || !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// File is empty or doesn't exist, try reading from the old path.
|
||||
|
||||
const oldPath = "/var/packages/Tailscale/etc/tailscaled.state"
|
||||
if _, err := os.Stat(oldPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chown(oldPath, os.Getuid(), os.Getgid()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(oldPath, p); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipnServerOpts() (o ipnserver.Options) {
|
||||
// Allow changing the OS-specific IPN behavior for tests
|
||||
// so we can e.g. test Windows-specific behaviors on Linux.
|
||||
@@ -225,6 +253,9 @@ func run() error {
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
if err := trySynologyMigration(args.statepath); err != nil {
|
||||
log.Printf("error in synology migration: %v", err)
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
@@ -335,9 +366,9 @@ func shouldWrapNetstack() bool {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
case "windows", "darwin", "freebsd":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients).
|
||||
// affect the GUI clients), and on FreeBSD.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
98
cmd/testcontrol/testcontrol.go
Normal file
98
cmd/testcontrol/testcontrol.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
// Program testcontrol runs a simple test control server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
flagNFake = flag.Int("nfake", 0, "number of fake nodes to add to network")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var t fakeTB
|
||||
derpMap := integration.RunDERPAndSTUN(t, logger.Discard, "127.0.0.1")
|
||||
|
||||
control := &testcontrol.Server{
|
||||
DERPMap: derpMap,
|
||||
ExplicitBaseURL: "http://127.0.0.1:9911",
|
||||
}
|
||||
for i := 0; i < *flagNFake; i++ {
|
||||
control.AddFakeNode()
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", control)
|
||||
addr := "127.0.0.1:9911"
|
||||
log.Printf("listening on %s", addr)
|
||||
err := http.ListenAndServe(addr, mux)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type fakeTB struct {
|
||||
*testing.T
|
||||
}
|
||||
|
||||
func (t fakeTB) Cleanup(_ func()) {}
|
||||
func (t fakeTB) Error(args ...interface{}) {
|
||||
t.Fatal(args...)
|
||||
}
|
||||
func (t fakeTB) Errorf(format string, args ...interface{}) {
|
||||
t.Fatalf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Fail() {
|
||||
t.Fatal("failed")
|
||||
}
|
||||
func (t fakeTB) FailNow() {
|
||||
t.Fatal("failed")
|
||||
}
|
||||
func (t fakeTB) Failed() bool {
|
||||
return false
|
||||
}
|
||||
func (t fakeTB) Fatal(args ...interface{}) {
|
||||
log.Fatal(args...)
|
||||
}
|
||||
func (t fakeTB) Fatalf(format string, args ...interface{}) {
|
||||
log.Fatalf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Helper() {}
|
||||
func (t fakeTB) Log(args ...interface{}) {
|
||||
log.Print(args...)
|
||||
}
|
||||
func (t fakeTB) Logf(format string, args ...interface{}) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Name() string {
|
||||
return "faketest"
|
||||
}
|
||||
func (t fakeTB) Setenv(key string, value string) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (t fakeTB) Skip(args ...interface{}) {
|
||||
t.Fatal("skipped")
|
||||
}
|
||||
func (t fakeTB) SkipNow() {
|
||||
t.Fatal("skipnow")
|
||||
}
|
||||
func (t fakeTB) Skipf(format string, args ...interface{}) {
|
||||
t.Logf(format, args...)
|
||||
t.Fatal("skipped")
|
||||
}
|
||||
func (t fakeTB) Skipped() bool {
|
||||
return false
|
||||
}
|
||||
func (t fakeTB) TempDir() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -75,10 +75,3 @@ func TestStatusEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -33,6 +32,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
@@ -47,9 +47,7 @@ import (
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -184,53 +182,13 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
pinger: opts.Pinger,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
} else {
|
||||
c.SetHostinfo(opts.Hostinfo)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
func NewHostinfo() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
hostname = dnsname.FirstLabel(hostname)
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
Package: packageType(),
|
||||
GoArch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
func packageType() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
|
||||
return "choco"
|
||||
}
|
||||
case "darwin":
|
||||
// Using tailscaled or IPNExtension?
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Base(exe)
|
||||
case "linux":
|
||||
// Report whether this is in a snap.
|
||||
// See https://snapcraft.io/docs/environment-variables
|
||||
// We just look at two somewhat arbitrarily.
|
||||
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
|
||||
return "snap"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetHostinfo clones the provided Hostinfo and remembers it for the
|
||||
// next update. It reports whether the Hostinfo has changed.
|
||||
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
@@ -872,15 +830,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
nm.LocalPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
// Code elsewhere prints netmap diffs every time, so this
|
||||
// occasional full dump, plus incremental diffs, should do
|
||||
// the job.
|
||||
// Occasionally print the netmap header.
|
||||
// This is handy for debugging, and our logs processing
|
||||
// pipeline depends on it. (TODO: Remove this dependency.)
|
||||
// Code elsewhere prints netmap diffs every time they are received.
|
||||
now := c.timeNow()
|
||||
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
|
||||
c.lastPrintMap = now
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -1303,3 +1260,59 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
|
||||
// with ping response data.
|
||||
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
|
||||
var err error
|
||||
if pr.URL == "" {
|
||||
return errors.New("invalid PingRequest with no URL")
|
||||
}
|
||||
if pr.IP.IsZero() {
|
||||
return errors.New("PingRequest without IP")
|
||||
}
|
||||
if !strings.Contains(pr.Types, "TSMP") {
|
||||
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
|
||||
// Currently does not check for error since we just return if it fails.
|
||||
err = postPingResult(now, logf, c, pr, res)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
|
||||
if res.Err != "" {
|
||||
return errors.New(res.Err)
|
||||
}
|
||||
duration := time.Since(now)
|
||||
if pr.Log {
|
||||
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
jsonPingRes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Send the results of the Ping, back to control URL.
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
|
||||
}
|
||||
if pr.Log {
|
||||
logf("tsmpPing: sending ping results to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,15 +6,20 @@ package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
@@ -56,7 +61,7 @@ func TestNewDirect(t *testing.T) {
|
||||
if changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
|
||||
}
|
||||
hi = NewHostinfo()
|
||||
hi = hostinfo.New()
|
||||
hi.Hostname = "different host name"
|
||||
changed = c.SetHostinfo(hi)
|
||||
if !changed {
|
||||
@@ -92,14 +97,55 @@ func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
func TestTsmpPing(t *testing.T) {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (wgkey.Private, error) {
|
||||
return key, nil
|
||||
},
|
||||
}
|
||||
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pingRes := &ipnstate.PingResult{
|
||||
IP: "123.456.7890",
|
||||
Err: "",
|
||||
NodeName: "testnode",
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body := new(ipnstate.PingResult)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pingRes.IP != body.IP {
|
||||
t.Fatalf("PingResult did not have the correct IP : got %v, expected : %v", body.IP, pingRes.IP)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
pr := &tailcfg.PingRequest{
|
||||
URL: ts.URL,
|
||||
}
|
||||
|
||||
err = postPingResult(now, t.Logf, c.httpc, pr, pingRes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
@@ -1275,7 +1275,7 @@ func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
return
|
||||
}
|
||||
if m, ok := prev.(multiForwarder); ok {
|
||||
if _, ok := m[fwd]; !ok {
|
||||
if _, ok := m[fwd]; ok {
|
||||
// Duplicate registration of same forwarder in set; ignore.
|
||||
return
|
||||
}
|
||||
|
||||
@@ -712,6 +712,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
// Adding a dup for a user.
|
||||
wantCounter(&s.multiForwarderCreated, 0)
|
||||
s.AddPacketForwarder(u1, testFwd(100))
|
||||
s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
|
||||
3
go.mod
3
go.mod
@@ -40,7 +40,7 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
|
||||
golang.org/x/tools v0.1.2
|
||||
@@ -51,5 +51,4 @@ require (
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
5
go.sum
5
go.sum
@@ -810,8 +810,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo=
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/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-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
@@ -987,5 +988,3 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jC
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
@@ -4,20 +4,64 @@
|
||||
|
||||
// Package hostinfo answers questions about the host environment that Tailscale is
|
||||
// running on.
|
||||
//
|
||||
// TODO(bradfitz): move more of control/controlclient/hostinfo_* into this package.
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
// New returns a partially populated Hostinfo for the current host.
|
||||
func New() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
hostname = dnsname.FirstLabel(hostname)
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
Package: packageType(),
|
||||
GoArch: runtime.GOARCH,
|
||||
DeviceModel: deviceModel(),
|
||||
}
|
||||
}
|
||||
|
||||
func packageType() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
|
||||
return "choco"
|
||||
}
|
||||
case "darwin":
|
||||
// Using tailscaled or IPNExtension?
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Base(exe)
|
||||
case "linux":
|
||||
// Report whether this is in a snap.
|
||||
// See https://snapcraft.io/docs/environment-variables
|
||||
// We just look at two somewhat arbitrarily.
|
||||
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
|
||||
return "snap"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// EnvType represents a known environment type.
|
||||
// The empty string, the default, means unknown.
|
||||
type EnvType string
|
||||
@@ -28,6 +72,7 @@ const (
|
||||
Heroku = EnvType("hr")
|
||||
AzureAppService = EnvType("az")
|
||||
AWSFargate = EnvType("fg")
|
||||
FlyDotIo = EnvType("fly")
|
||||
)
|
||||
|
||||
var envType atomic.Value // of EnvType
|
||||
@@ -41,6 +86,16 @@ func GetEnvType() EnvType {
|
||||
return e
|
||||
}
|
||||
|
||||
var deviceModelAtomic atomic.Value // of string
|
||||
|
||||
// SetDeviceModel sets the device model for use in Hostinfo updates.
|
||||
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
|
||||
|
||||
func deviceModel() string {
|
||||
s, _ := deviceModelAtomic.Load().(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func getEnvType() EnvType {
|
||||
if inKnative() {
|
||||
return KNative
|
||||
@@ -57,11 +112,14 @@ func getEnvType() EnvType {
|
||||
if inAWSFargate() {
|
||||
return AWSFargate
|
||||
}
|
||||
if inFlyDotIo() {
|
||||
return FlyDotIo
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// InContainer reports whether we're running in a container.
|
||||
func InContainer() bool {
|
||||
// inContainer reports whether we're running in a container.
|
||||
func inContainer() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
@@ -126,3 +184,10 @@ func inAWSFargate() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inFlyDotIo() bool {
|
||||
if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,22 +5,28 @@
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package controlclient
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
osVersion = osVersionLinux
|
||||
|
||||
if v, _ := os.ReadFile("/sys/firmware/devicetree/base/model"); len(v) > 0 {
|
||||
// Look up "Raspberry Pi 4 Model B Rev 1.2",
|
||||
// etc. Usually set on ARM SBCs.
|
||||
SetDeviceModel(strings.Trim(string(v), "\x00\r\n\t "))
|
||||
}
|
||||
}
|
||||
|
||||
func osVersionLinux() string {
|
||||
@@ -55,10 +61,10 @@ func osVersionLinux() string {
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
if hostinfo.InContainer() {
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
if env := hostinfo.GetEnvType(); env != "" {
|
||||
if env := GetEnvType(); env != "" {
|
||||
fmt.Fprintf(&attrBuf, "; env=%s", env)
|
||||
}
|
||||
attr := attrBuf.String()
|
||||
29
hostinfo/hostinfo_test.go
Normal file
29
hostinfo/hostinfo_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 hostinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
hi := New()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
@@ -730,7 +731,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostinfo := controlclient.NewHostinfo()
|
||||
hostinfo := hostinfo.New()
|
||||
hostinfo.BackendLogID = b.backendLogID
|
||||
hostinfo.FrontendLogID = opts.FrontendLogID
|
||||
|
||||
@@ -783,8 +784,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
b.inServerMode = b.prefs.ForceDaemon
|
||||
b.serverURL = b.prefs.ControlURLOrDefault()
|
||||
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
|
||||
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
|
||||
if b.inServerMode || runtime.GOOS == "windows" {
|
||||
b.logf("Start: serverMode=%v", b.inServerMode)
|
||||
}
|
||||
@@ -851,6 +850,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
DiscoPublicKey: discoPublic,
|
||||
DebugFlags: debugFlags,
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b.e,
|
||||
|
||||
// Don't warn about broken Linux IP forwading when
|
||||
// netstack is being used.
|
||||
@@ -961,7 +961,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.logf("netmap packet filter: %v filters", len(packetFilter))
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
@@ -1579,7 +1579,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
newHi.RoutableIPs = append([]netaddr.IPPrefix(nil), b.prefs.AdvertiseRoutes...)
|
||||
applyPrefsToHostinfo(newHi, newp)
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
@@ -1838,6 +1837,17 @@ func (b *LocalBackend) authReconfig() {
|
||||
if err != nil {
|
||||
b.logf("[unexpected] non-FQDN route suffix %q", suffix)
|
||||
}
|
||||
|
||||
// Create map entry even if len(resolvers) == 0; Issue 2706.
|
||||
// This lets the control plane send ExtraRecords for which we
|
||||
// can authoritatively answer "name not exists" for when the
|
||||
// control plane also sends this explicit but empty route
|
||||
// making it as something we handle.
|
||||
//
|
||||
// While we're already populating it, might as well size the
|
||||
// slice appropriately.
|
||||
dcfg.Routes[fqdn] = make([]netaddr.IPPort, 0, len(resolvers))
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
res, err := parseResolver(resolver)
|
||||
if err != nil {
|
||||
@@ -2217,6 +2227,8 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
|
||||
if m := prefs.DeviceModel; m != "" {
|
||||
hi.DeviceModel = m
|
||||
}
|
||||
hi.RoutableIPs = append(prefs.AdvertiseRoutes[:0:0], prefs.AdvertiseRoutes...)
|
||||
hi.RequestTags = append(prefs.AdvertiseTags[:0:0], prefs.AdvertiseTags...)
|
||||
hi.ShieldsUp = prefs.ShieldsUp
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (darwin && ts_macext) || (ios && ts_macext)
|
||||
// +build darwin,ts_macext ios,ts_macext
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
// +build ts_macext
|
||||
// +build darwin ios
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -581,6 +583,28 @@ func (s *server) writeToClients(n ipn.Notify) {
|
||||
}
|
||||
}
|
||||
|
||||
// tryWindowsAppDataMigration attempts to copy the Windows state file
|
||||
// from its old location to the new location. (Issue 2856)
|
||||
//
|
||||
// Tailscale 1.14 and before stored state under %LocalAppData%
|
||||
// (usually "C:\WINDOWS\system32\config\systemprofile\AppData\Local"
|
||||
// when tailscaled.exe is running as a non-user system service).
|
||||
// However it is frequently cleared for almost any reason: Windows
|
||||
// updates, System Restore, even various System Cleaner utilities.
|
||||
//
|
||||
// Returns a string of the path to use for the state file.
|
||||
// This will be a fallback %LocalAppData% path if migration fails,
|
||||
// a %ProgramData% path otherwise.
|
||||
func tryWindowsAppDataMigration(logf logger.Logf, path string) string {
|
||||
if path != paths.DefaultTailscaledStateFile() {
|
||||
// If they're specifying a non-default path, just trust that they know
|
||||
// what they are doing.
|
||||
return path
|
||||
}
|
||||
oldFile := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf")
|
||||
return paths.TryConfigFileMigration(logf, oldFile, path)
|
||||
}
|
||||
|
||||
// Run runs a Tailscale backend service.
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
@@ -613,14 +637,18 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
store, err = ipn.NewFileStore(opts.StatePath)
|
||||
path := opts.StatePath
|
||||
if runtime.GOOS == "windows" {
|
||||
path = tryWindowsAppDataMigration(logf, path)
|
||||
}
|
||||
store, err = ipn.NewFileStore(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err)
|
||||
return fmt.Errorf("ipn.NewFileStore(%q): %v", path, err)
|
||||
}
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err)
|
||||
return fmt.Errorf("calling ReadState on %s: %w", path, err)
|
||||
}
|
||||
key := string(autoStartKey)
|
||||
if strings.HasPrefix(key, "user-") {
|
||||
|
||||
450
ipn/localapi/cert.go
Normal file
450
ipn/localapi/cert.go
Normal file
@@ -0,0 +1,450 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Process-wide cache. (A new *Handler is created per connection,
|
||||
// effectively per request)
|
||||
var (
|
||||
// acmeMu guards all ACME operations, so concurrent requests
|
||||
// for certs don't slam ACME. The first will go through and
|
||||
// populate the on-disk cache and the rest should use that.
|
||||
acmeMu sync.Mutex
|
||||
|
||||
renewMu sync.Mutex // lock order: don't hold acmeMu and renewMu at the same time
|
||||
lastRenewCheck = map[string]time.Time{}
|
||||
)
|
||||
|
||||
func (h *Handler) certDir() (string, error) {
|
||||
base := paths.DefaultTailscaledStateFile()
|
||||
if base == "" {
|
||||
return "", errors.New("no default DefaultTailscaledStateFile")
|
||||
}
|
||||
full := filepath.Join(filepath.Dir(base), "certs")
|
||||
if err := os.MkdirAll(full, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return full, nil
|
||||
}
|
||||
|
||||
var acmeDebug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ACME"))
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "cert access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dir, err := h.certDir()
|
||||
if err != nil {
|
||||
h.logf("certDir: %v", err)
|
||||
http.Error(w, "failed to get cert dir", 500)
|
||||
return
|
||||
}
|
||||
|
||||
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
|
||||
if domain == r.URL.Path {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
|
||||
traceACME := func(v interface{}) {
|
||||
if !acmeDebug {
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", "\t")
|
||||
log.Printf("acme %T: %s", v, j)
|
||||
}
|
||||
|
||||
if pair, ok := h.getCertPEMCached(dir, domain, now); ok {
|
||||
future := now.AddDate(0, 0, 14)
|
||||
if h.shouldStartDomainRenewal(dir, domain, future) {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go h.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
|
||||
}
|
||||
serveKeyPair(w, r, pair)
|
||||
return
|
||||
}
|
||||
|
||||
pair, err := h.getCertPEM(r.Context(), logf, traceACME, dir, domain, now)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
http.Error(w, fmt.Sprint(err), 500)
|
||||
return
|
||||
}
|
||||
serveKeyPair(w, r, pair)
|
||||
}
|
||||
|
||||
func (h *Handler) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
now := time.Now()
|
||||
if last, ok := lastRenewCheck[domain]; ok && now.Sub(last) < time.Minute {
|
||||
// We checked very recently. Don't bother reparsing &
|
||||
// validating the x509 cert.
|
||||
return false
|
||||
}
|
||||
lastRenewCheck[domain] = now
|
||||
_, ok := h.getCertPEMCached(dir, domain, future)
|
||||
return !ok
|
||||
}
|
||||
|
||||
func serveKeyPair(w http.ResponseWriter, r *http.Request, p *keyPair) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
switch r.URL.Query().Get("type") {
|
||||
case "", "crt", "cert":
|
||||
w.Write(p.certPEM)
|
||||
case "key":
|
||||
w.Write(p.keyPEM)
|
||||
case "pair":
|
||||
w.Write(p.keyPEM)
|
||||
w.Write(p.certPEM)
|
||||
default:
|
||||
http.Error(w, `invalid type; want "cert" (default), "key", or "pair"`, 400)
|
||||
}
|
||||
}
|
||||
|
||||
type keyPair struct {
|
||||
certPEM []byte
|
||||
keyPEM []byte
|
||||
cached bool
|
||||
}
|
||||
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
// getCertPEMCached returns a non-nil keyPair and true if a cached
|
||||
// keypair for domain exists on disk in dir that is valid at the
|
||||
// provided now time.
|
||||
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
|
||||
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
|
||||
certPEM, _ := os.ReadFile(certFile(dir, domain))
|
||||
if validCertPEM(domain, keyPEM, certPEM, now) {
|
||||
return &keyPair{certPEM: certPEM, keyPEM: keyPEM, cached: true}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(interface{}), dir, domain string, now time.Time) (*keyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
if p, ok := h.getCertPEMCached(dir, domain, now); ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
key, err := acmeKey(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acmeKey: %w", err)
|
||||
}
|
||||
ac := &acme.Client{Key: key}
|
||||
|
||||
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
switch {
|
||||
case err == nil:
|
||||
// Great, already registered.
|
||||
logf("already had ACME account.")
|
||||
case err == acme.ErrNoAccount:
|
||||
a, err = ac.Register(ctx, new(acme.Account), acme.AcceptTOS)
|
||||
if err == acme.ErrAccountAlreadyExists {
|
||||
// Potential race. Double check.
|
||||
a, err = ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme.Register: %w", err)
|
||||
}
|
||||
logf("registered ACME account.")
|
||||
traceACME(a)
|
||||
default:
|
||||
return nil, fmt.Errorf("acme.GetReg: %w", err)
|
||||
|
||||
}
|
||||
if a.Status != acme.StatusValid {
|
||||
return nil, fmt.Errorf("unexpected ACME account status %q", a.Status)
|
||||
}
|
||||
|
||||
// Before hitting LetsEncrypt, see if this is a domain that Tailscale will do DNS challenges for.
|
||||
st := h.b.StatusWithoutPeers()
|
||||
if err := checkCertDomain(st, domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order, err := ac.AuthorizeOrder(ctx, []acme.AuthzID{{Type: "dns", Value: domain}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceACME(order)
|
||||
|
||||
for _, aurl := range order.AuthzURLs {
|
||||
az, err := ac.GetAuthorization(ctx, aurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceACME(az)
|
||||
for _, ch := range az.Challenges {
|
||||
if ch.Type == "dns-01" {
|
||||
rec, err := ac.DNS01ChallengeRecord(ch.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := "_acme-challenge." + domain
|
||||
|
||||
var resolver net.Resolver
|
||||
var ok bool
|
||||
txts, _ := resolver.LookupTXT(ctx, key)
|
||||
for _, txt := range txts {
|
||||
if txt == rec {
|
||||
ok = true
|
||||
logf("TXT record already existed")
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
err = h.b.SetDNS(ctx, key, rec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SetDNS %q => %q: %w", key, rec, err)
|
||||
}
|
||||
logf("did SetDNS")
|
||||
}
|
||||
|
||||
chal, err := ac.Accept(ctx, ch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Accept: %v", err)
|
||||
}
|
||||
traceACME(chal)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wait0 := time.Now()
|
||||
orderURI := order.URI
|
||||
for {
|
||||
order, err = ac.WaitOrder(ctx, orderURI)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if oe, ok := err.(*acme.OrderError); ok && oe.Status == acme.StatusInvalid {
|
||||
if time.Since(wait0) > 2*time.Minute {
|
||||
return nil, errors.New("timeout waiting for order to not be invalid")
|
||||
}
|
||||
log.Printf("order invalid; waiting...")
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("WaitOrder: %v", err)
|
||||
}
|
||||
traceACME(order)
|
||||
|
||||
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var privPEM bytes.Buffer
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csr, err := certRequest(certPrivKey, domain, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
der, _, err := ac.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateOrder: %v", err)
|
||||
}
|
||||
|
||||
var certPEM bytes.Buffer
|
||||
for _, b := range der {
|
||||
pb := &pem.Block{Type: "CERTIFICATE", Bytes: b}
|
||||
if err := pem.Encode(&certPEM, pb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &keyPair{certPEM: certPEM.Bytes(), keyPEM: privPEM.Bytes()}, nil
|
||||
}
|
||||
|
||||
// certRequest generates a CSR for the given common name cn and optional SANs.
|
||||
func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) {
|
||||
req := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
DNSNames: san,
|
||||
ExtraExtensions: ext,
|
||||
}
|
||||
return x509.CreateCertificateRequest(rand.Reader, req, key)
|
||||
}
|
||||
|
||||
func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
|
||||
b, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||
return pem.Encode(w, pb)
|
||||
}
|
||||
|
||||
// parsePrivateKey is a copy of x/crypto/acme's parsePrivateKey.
|
||||
//
|
||||
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
||||
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
||||
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||
//
|
||||
// Inspired by parsePrivateKey in crypto/tls/tls.go.
|
||||
func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping")
|
||||
}
|
||||
}
|
||||
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||
}
|
||||
|
||||
func acmeKey(dir string) (crypto.Signer, error) {
|
||||
pemName := filepath.Join(dir, "acme-account.key.pem")
|
||||
if v, err := ioutil.ReadFile(pemName); err == nil {
|
||||
priv, _ := pem.Decode(v)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: invalid account key found in cache")
|
||||
}
|
||||
return parsePrivateKey(priv.Bytes)
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pemBuf bytes.Buffer
|
||||
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
|
||||
if len(keyPEM) == 0 || len(certPEM) == 0 {
|
||||
return false
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var leaf *x509.Certificate
|
||||
intermediates := x509.NewCertPool()
|
||||
for i, certDER := range tlsCert.Certificate {
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if i == 0 {
|
||||
leaf = cert
|
||||
} else {
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
}
|
||||
if leaf == nil {
|
||||
return false
|
||||
}
|
||||
_, err = leaf.Verify(x509.VerifyOptions{
|
||||
DNSName: domain,
|
||||
CurrentTime: now,
|
||||
Intermediates: intermediates,
|
||||
})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func checkCertDomain(st *ipnstate.Status, domain string) error {
|
||||
if domain == "" {
|
||||
return errors.New("missing domain name")
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if d == domain {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Transitional way while server doesn't yet populate CertDomains: also permit the client
|
||||
// attempting Self.DNSName.
|
||||
okay := st.CertDomains[:len(st.CertDomains):len(st.CertDomains)]
|
||||
if st.Self != nil {
|
||||
if v := strings.Trim(st.Self.DNSName, "."); v != "" {
|
||||
if v == domain {
|
||||
return nil
|
||||
}
|
||||
okay = append(okay, v)
|
||||
}
|
||||
}
|
||||
switch len(okay) {
|
||||
case 0:
|
||||
return errors.New("your Tailscale account does not support getting TLS certs")
|
||||
case 1:
|
||||
return fmt.Errorf("invalid domain %q; only %q is permitted", domain, okay[0])
|
||||
default:
|
||||
return fmt.Errorf("invalid domain %q; must be one of %q", domain, okay)
|
||||
}
|
||||
}
|
||||
17
ipn/localapi/disabled_stubs.go
Normal file
17
ipn/localapi/disabled_stubs.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
//go:build ios || android
|
||||
// +build ios android
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "disabled on "+runtime.GOOS, http.StatusNotFound)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func randHex(n int) string {
|
||||
@@ -64,6 +65,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "server has no local backend", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Tailscale-Version", version.Long)
|
||||
if h.RequiredPassword != "" {
|
||||
_, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
@@ -83,6 +85,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveFilePut(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
|
||||
h.serveCert(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/paths"
|
||||
)
|
||||
|
||||
// ErrStateNotExist is returned by StateStore.ReadState when the
|
||||
@@ -97,6 +98,11 @@ func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path
|
||||
|
||||
// NewFileStore returns a new file store that persists to path.
|
||||
func NewFileStore(path string) (*FileStore, error) {
|
||||
// We unconditionally call this to ensure that our perms are correct
|
||||
if err := paths.MkStateDir(filepath.Dir(path)); err != nil {
|
||||
return nil, fmt.Errorf("creating state directory: %w", err)
|
||||
}
|
||||
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
|
||||
// Treat an empty file as a missing file.
|
||||
@@ -110,7 +116,6 @@ func NewFileStore(path string) (*FileStore, error) {
|
||||
if os.IsNotExist(err) {
|
||||
// Write out an initial file, to verify that we can write
|
||||
// to the path.
|
||||
os.MkdirAll(filepath.Dir(path), 0755) // best effort
|
||||
if err = atomicfile.WriteFile(path, []byte("{}"), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "Logs")
|
||||
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
|
||||
@@ -135,12 +135,33 @@ func logsDir(logf logger.Logf) string {
|
||||
}
|
||||
}
|
||||
|
||||
// STATE_DIRECTORY is set by systemd 240+ but we support older
|
||||
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
|
||||
systemdStateDir := os.Getenv("STATE_DIRECTORY")
|
||||
if systemdStateDir != "" {
|
||||
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
|
||||
return systemdStateDir
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if version.CmdName() == "tailscaled" {
|
||||
// In the common case, when tailscaled is run as the Local System (as a service),
|
||||
// we want to use %ProgramData% (C:\ProgramData\Tailscale), aside the
|
||||
// system state config with the machine key, etc. But if that directory's
|
||||
// not accessible, then it's probably because the user is running tailscaled
|
||||
// as a regular user (perhaps in userspace-networking/SOCK5 mode) and we should
|
||||
// just use the %LocalAppData% instead. In a user context, %LocalAppData% isn't
|
||||
// subject to random deletions from Windows system updates.
|
||||
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
|
||||
if winProgramDataAccessible(dir) {
|
||||
logf("logpolicy: using dir %v", dir)
|
||||
return dir
|
||||
}
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale")
|
||||
logf("logpolicy: using LocalAppData dir %v", dir)
|
||||
return dir
|
||||
case "linux":
|
||||
// STATE_DIRECTORY is set by systemd 240+ but we support older
|
||||
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
|
||||
systemdStateDir := os.Getenv("STATE_DIRECTORY")
|
||||
if systemdStateDir != "" {
|
||||
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
|
||||
return systemdStateDir
|
||||
}
|
||||
}
|
||||
|
||||
// Default to e.g. /var/lib/tailscale or /var/db/tailscale on Unix.
|
||||
@@ -191,6 +212,23 @@ func redirectStderrToLogPanics() bool {
|
||||
return runningUnderSystemd() || os.Getenv("TS_PLEASE_PANIC") != ""
|
||||
}
|
||||
|
||||
// winProgramDataAccessible reports whether the directory (assumed to
|
||||
// be a Windows %ProgramData% directory) is accessible to the current
|
||||
// process. It's created if needed.
|
||||
func winProgramDataAccessible(dir string) bool {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
// TODO: windows ACLs
|
||||
return false
|
||||
}
|
||||
// The C:\ProgramData\Tailscale directory should be locked down
|
||||
// by with ACLs to only be readable by the local system so a
|
||||
// regular user shouldn't be able to do this operation:
|
||||
if _, err := os.ReadDir(dir); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// tryFixLogStateLocation is a temporary fixup for
|
||||
// https://github.com/tailscale/tailscale/issues/247 . We accidentally
|
||||
// wrote logging state files to /, and then later to $CACHE_DIRECTORY
|
||||
@@ -372,14 +410,44 @@ func New(collection string) *Policy {
|
||||
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
|
||||
|
||||
// The Windows service previously ran as tailscale-ipn.exe, so
|
||||
// let's keep using that log base name if it exists.
|
||||
if runtime.GOOS == "windows" && cmdName == "tailscaled" {
|
||||
const oldCmdName = "tailscale-ipn"
|
||||
oldPath := filepath.Join(dir, oldCmdName+".log.conf")
|
||||
if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() {
|
||||
cfgPath = oldPath
|
||||
cmdName = oldCmdName
|
||||
if runtime.GOOS == "windows" {
|
||||
switch cmdName {
|
||||
case "tailscaled":
|
||||
// Tailscale 1.14 and before stored state under %LocalAppData%
|
||||
// (usually "C:\WINDOWS\system32\config\systemprofile\AppData\Local"
|
||||
// when tailscaled.exe is running as a non-user system service).
|
||||
// However it is frequently cleared for almost any reason: Windows
|
||||
// updates, System Restore, even various System Cleaner utilities.
|
||||
//
|
||||
// The Windows service previously ran as tailscale-ipn.exe, so
|
||||
// machines which ran very old versions might still have their
|
||||
// log conf named %LocalAppData%\tailscale-ipn.log.conf
|
||||
//
|
||||
// Machines which started using Tailscale more recently will have
|
||||
// %LocalAppData%\tailscaled.log.conf
|
||||
//
|
||||
// Attempt to migrate the log conf to C:\ProgramData\Tailscale
|
||||
oldDir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale")
|
||||
|
||||
oldPath := filepath.Join(oldDir, "tailscaled.log.conf")
|
||||
if fi, err := os.Stat(oldPath); err != nil || !fi.Mode().IsRegular() {
|
||||
// *Only* if tailscaled.log.conf does not exist,
|
||||
// check for tailscale-ipn.log.conf
|
||||
oldPathOldCmd := filepath.Join(oldDir, "tailscale-ipn.log.conf")
|
||||
if fi, err := os.Stat(oldPathOldCmd); err == nil && fi.Mode().IsRegular() {
|
||||
oldPath = oldPathOldCmd
|
||||
}
|
||||
}
|
||||
|
||||
cfgPath = paths.TryConfigFileMigration(earlyLogf, oldPath, cfgPath)
|
||||
case "tailscale-ipn":
|
||||
for _, oldBase := range []string{"wg64.log.conf", "wg32.log.conf"} {
|
||||
oldConf := filepath.Join(dir, oldBase)
|
||||
if fi, err := os.Stat(oldConf); err == nil && fi.Mode().IsRegular() {
|
||||
cfgPath = paths.TryConfigFileMigration(earlyLogf, oldConf, cfgPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,15 @@ type Filch struct {
|
||||
alt *os.File
|
||||
altscan *bufio.Scanner
|
||||
recovered int64
|
||||
// buf is an initial buffer for altscan.
|
||||
// As of August 2021, 99.96% of all log lines
|
||||
// are below 4096 bytes in length.
|
||||
// Since this cutoff is arbitrary, instead of using 4096,
|
||||
// we subtract off the size of the rest of the struct
|
||||
// so that the whole struct takes 4096 bytes
|
||||
// (less on 32 bit platforms).
|
||||
// This reduces allocation waste.
|
||||
buf [4096 - 48]byte
|
||||
}
|
||||
|
||||
// TryReadline implements the logtail.Buffer interface.
|
||||
@@ -53,6 +62,7 @@ func (f *Filch) TryReadLine() ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
f.altscan = bufio.NewScanner(f.alt)
|
||||
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
|
||||
f.altscan.Split(splitLines)
|
||||
return f.scan()
|
||||
}
|
||||
@@ -188,6 +198,7 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
}
|
||||
if f.recovered > 0 {
|
||||
f.altscan = bufio.NewScanner(f.alt)
|
||||
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
|
||||
f.altscan.Split(splitLines)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type filchTest struct {
|
||||
@@ -169,3 +170,10 @@ func TestFilchStderr(t *testing.T) {
|
||||
t.Errorf("unexpected write to fake stderr: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSizeOf(t *testing.T) {
|
||||
s := unsafe.Sizeof(Filch{})
|
||||
if s > 4096 {
|
||||
t.Fatalf("Filch{} has size %d on %v, decrease size of buf field", s, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,9 +200,11 @@ func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
}
|
||||
|
||||
// drainPending drains and encodes a batch of logs from the buffer for upload.
|
||||
// It uses scratch as its initial buffer.
|
||||
// If no logs are available, drainPending blocks until logs are available.
|
||||
func (l *Logger) drainPending() (res []byte) {
|
||||
buf := new(bytes.Buffer)
|
||||
func (l *Logger) drainPending(scratch []byte) (res []byte) {
|
||||
buf := bytes.NewBuffer(scratch[:0])
|
||||
buf.WriteByte('[')
|
||||
entries := 0
|
||||
|
||||
var batchDone bool
|
||||
@@ -242,28 +244,15 @@ func (l *Logger) drainPending() (res []byte) {
|
||||
b = l.encodeText(b, true)
|
||||
}
|
||||
|
||||
switch {
|
||||
case entries == 0:
|
||||
buf.Write(b)
|
||||
case entries == 1:
|
||||
buf2 := new(bytes.Buffer)
|
||||
buf2.WriteByte('[')
|
||||
buf2.Write(buf.Bytes())
|
||||
buf2.WriteByte(',')
|
||||
buf2.Write(b)
|
||||
buf.Reset()
|
||||
buf.Write(buf2.Bytes())
|
||||
default:
|
||||
if entries > 0 {
|
||||
buf.WriteByte(',')
|
||||
buf.Write(b)
|
||||
}
|
||||
buf.Write(b)
|
||||
entries++
|
||||
}
|
||||
|
||||
if entries > 1 {
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
buf.WriteByte(']')
|
||||
if buf.Len() <= len("[]") {
|
||||
return nil
|
||||
}
|
||||
return buf.Bytes()
|
||||
@@ -273,8 +262,9 @@ func (l *Logger) drainPending() (res []byte) {
|
||||
func (l *Logger) uploading(ctx context.Context) {
|
||||
defer close(l.shutdownDone)
|
||||
|
||||
scratch := make([]byte, 4096) // reusable buffer to write into
|
||||
for {
|
||||
body := l.drainPending()
|
||||
body := l.drainPending(scratch)
|
||||
origlen := -1 // sentinel value: uncompressed
|
||||
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
|
||||
if l.zstdEncoder != nil && len(body) > 256 {
|
||||
|
||||
@@ -117,12 +117,7 @@ func TestEncodeAndUploadMessages(t *testing.T) {
|
||||
io.WriteString(l, tt.log)
|
||||
body := <-ts.uploaded
|
||||
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
data := unmarshalOne(t, body)
|
||||
got := data["text"]
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
|
||||
@@ -154,11 +149,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
// JSON log message already contains a logtail field.
|
||||
io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
|
||||
body := <-ts.uploaded
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data := unmarshalOne(t, body)
|
||||
errorHasLogtail, ok := data["error_has_logtail"]
|
||||
if ok {
|
||||
if errorHasLogtail != "LOGTAIL" {
|
||||
@@ -186,11 +177,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
l.skipClientTime = true
|
||||
io.WriteString(l, "text")
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data = unmarshalOne(t, body)
|
||||
_, ok = data["logtail"]
|
||||
if ok {
|
||||
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
|
||||
@@ -204,11 +191,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
longStr := strings.Repeat("0", 512)
|
||||
io.WriteString(l, longStr)
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data = unmarshalOne(t, body)
|
||||
text, ok := data["text"]
|
||||
if !ok {
|
||||
t.Errorf("lowMem: no text %v", data)
|
||||
@@ -219,7 +202,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
err = l.Shutdown(context.Background())
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -326,3 +309,16 @@ func TestPublicIDUnmarshalText(t *testing.T) {
|
||||
t.Errorf("allocs = %v; want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalOne(t *testing.T, body []byte) map[string]interface{} {
|
||||
t.Helper()
|
||||
var entries []map[string]interface{}
|
||||
err := json.Unmarshal(body, &entries)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected one entry, got %d", len(entries))
|
||||
}
|
||||
return entries[0]
|
||||
}
|
||||
|
||||
@@ -216,7 +216,8 @@ func (m directManager) restoreBackup() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := m.fs.Stat(resolvConf); err != nil && !os.IsNotExist(err) {
|
||||
_, err = m.fs.Stat(resolvConf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
@@ -259,7 +260,7 @@ func (m directManager) SetDNS(config OSConfig) error {
|
||||
// try to manage DNS through resolved when it's around, but as a
|
||||
// best-effort fallback if we messed up the detection, try to
|
||||
// restart resolved to make the system configuration consistent.
|
||||
if isResolvedRunning() {
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
@@ -319,7 +320,7 @@ func (m directManager) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if isResolvedRunning() {
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
}
|
||||
|
||||
@@ -385,3 +386,12 @@ func (fs directFS) ReadFile(name string) ([]byte, error) {
|
||||
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
return ioutil.WriteFile(fs.path(name), contents, perm)
|
||||
}
|
||||
|
||||
// runningAsGUIDesktopUser reports whether it seems that this code is
|
||||
// being run as a regular user on a Linux desktop. This is a quick
|
||||
// hack to fix Issue 2672 where PolicyKit pops up a GUI dialog asking
|
||||
// to proceed we do a best effort attempt to restart
|
||||
// systemd-resolved.service. There's surely a better way.
|
||||
func runningAsGUIDesktopUser() bool {
|
||||
return os.Getuid() != 0 && os.Getenv("DISPLAY") != ""
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
@@ -65,44 +64,13 @@ func getTxID(packet []byte) txid {
|
||||
}
|
||||
|
||||
dnsid := binary.BigEndian.Uint16(packet[0:2])
|
||||
qcount := binary.BigEndian.Uint16(packet[4:6])
|
||||
if qcount == 0 {
|
||||
return txid(dnsid)
|
||||
}
|
||||
|
||||
offset := headerBytes
|
||||
for i := uint16(0); i < qcount; i++ {
|
||||
// Note: this relies on the fact that names are not compressed in questions,
|
||||
// so they are guaranteed to end with a NUL byte.
|
||||
//
|
||||
// Justification:
|
||||
// RFC 1035 doesn't seem to explicitly prohibit compressing names in questions,
|
||||
// but this is exceedingly unlikely to be done in practice. A DNS request
|
||||
// with multiple questions is ill-defined (which questions do the header flags apply to?)
|
||||
// and a single question would have to contain a pointer to an *answer*,
|
||||
// which would be excessively smart, pointless (an answer can just as well refer to the question)
|
||||
// and perhaps even prohibited: a draft RFC (draft-ietf-dnsind-local-compression-05) states:
|
||||
//
|
||||
// > It is important that these pointers always point backwards.
|
||||
//
|
||||
// This is said in summarizing RFC 1035, although that phrase does not appear in the original RFC.
|
||||
// Additionally, (https://cr.yp.to/djbdns/notes.html) states:
|
||||
//
|
||||
// > The precise rule is that a name can be compressed if it is a response owner name,
|
||||
// > the name in NS data, the name in CNAME data, the name in PTR data, the name in MX data,
|
||||
// > or one of the names in SOA data.
|
||||
namebytes := bytes.IndexByte(packet[offset:], 0)
|
||||
// ... | name | NUL | type | class
|
||||
// ?? 1 2 2
|
||||
offset = offset + namebytes + 5
|
||||
if len(packet) < offset {
|
||||
// Corrupt packet; don't crash.
|
||||
return txid(dnsid)
|
||||
}
|
||||
}
|
||||
|
||||
hash := crc32.ChecksumIEEE(packet[headerBytes:offset])
|
||||
return (txid(hash) << 32) | txid(dnsid)
|
||||
// Previously, we hashed the question and combined it with the original txid
|
||||
// which was useful when concurrent queries were multiplexed on a single
|
||||
// local source port. We encountered some situations where the DNS server
|
||||
// canonicalizes the question in the response (uppercase converted to
|
||||
// lowercase in this case), which resulted in responses that we couldn't
|
||||
// match to the original request due to hash mismatches.
|
||||
return txid(dnsid)
|
||||
}
|
||||
|
||||
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (darwin && ts_macext) || (ios && ts_macext)
|
||||
// +build darwin,ts_macext ios,ts_macext
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
// +build ts_macext
|
||||
// +build darwin ios
|
||||
|
||||
package resolver
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
@@ -66,6 +67,58 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToIPLowercase returns a handler function which canonicalizes responses
|
||||
// by lowercasing the question and answer names, and responds
|
||||
// to queries of type A it receives with an A record containing ipv4,
|
||||
// to queries of type AAAA with an AAAA record containing ipv6,
|
||||
// to queries of type NS with an NS record containg name.
|
||||
func resolveToIPLowercase(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
|
||||
if len(req.Question) != 1 {
|
||||
panic("not a single-question request")
|
||||
}
|
||||
m.Question[0].Name = strings.ToLower(m.Question[0].Name)
|
||||
question := req.Question[0]
|
||||
|
||||
var ans dns.RR
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
ans = &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: ipv4.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
ans = &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
AAAA: ipv6.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeNS:
|
||||
ans = &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ns: ns,
|
||||
}
|
||||
}
|
||||
|
||||
m.Answer = append(m.Answer, ans)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToTXT returns a handler function which responds to queries of type TXT
|
||||
// it receives with the strings in txts.
|
||||
func resolveToTXT(txts []string, ednsMaxSize uint16) dns.HandlerFunc {
|
||||
|
||||
@@ -440,6 +440,8 @@ func TestDelegate(t *testing.T) {
|
||||
records := []interface{}{
|
||||
"test.site.",
|
||||
resolveToIP(testipv4, testipv6, "dns.test.site."),
|
||||
"LCtesT.SiTe.",
|
||||
resolveToIPLowercase(testipv4, testipv6, "dns.test.site."),
|
||||
"nxdomain.site.", resolveToNXDOMAIN,
|
||||
"small.txt.", resolveToTXT(smallTXT, noEdns),
|
||||
"smalledns.txt.", resolveToTXT(smallTXT, 512),
|
||||
@@ -485,6 +487,21 @@ func TestDelegate(t *testing.T) {
|
||||
dnspacket("test.site.", dns.TypeNS, noEdns),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv4",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeA, noEdns),
|
||||
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv6",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeAAAA, noEdns),
|
||||
dnsResponse{ip: testipv6, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ns",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeNS, noEdns),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"nxdomain",
|
||||
dnspacket("nxdomain.site.", dns.TypeA, noEdns),
|
||||
|
||||
@@ -49,6 +49,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"12": {
|
||||
"RegionID": 12,
|
||||
"RegionCode": "r12",
|
||||
"RegionName": "r12",
|
||||
"Nodes": [
|
||||
{
|
||||
"Name": "12a",
|
||||
"RegionID": 12,
|
||||
"HostName": "derp12.tailscale.com",
|
||||
"IPv4": "216.128.144.130",
|
||||
"IPv6": "2001:19f0:5c01:289:5400:3ff:fe8d:cb5e"
|
||||
},
|
||||
{
|
||||
"Name": "12b",
|
||||
"RegionID": 12,
|
||||
"HostName": "derp12b.tailscale.com",
|
||||
"IPv4": "45.63.71.144",
|
||||
"IPv6": "2001:19f0:5c01:48a:5400:3ff:fe8d:cb5f"
|
||||
},
|
||||
{
|
||||
"Name": "12c",
|
||||
"RegionID": 12,
|
||||
"HostName": "derp12c.tailscale.com",
|
||||
"IPv4": "149.28.119.105",
|
||||
"IPv6": "2001:19f0:5c01:2cb:5400:3ff:fe8d:cb60"
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"RegionID": 2,
|
||||
"RegionCode": "r2",
|
||||
@@ -193,6 +221,20 @@
|
||||
"HostName": "derp9.tailscale.com",
|
||||
"IPv4": "207.148.3.137",
|
||||
"IPv6": "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"
|
||||
},
|
||||
{
|
||||
"Name": "9b",
|
||||
"RegionID": 9,
|
||||
"HostName": "derp9b.tailscale.com",
|
||||
"IPv4": "144.202.67.195",
|
||||
"IPv6": "2001:19f0:6401:eb5:5400:3ff:fe8d:6d9b"
|
||||
},
|
||||
{
|
||||
"Name": "9c",
|
||||
"RegionID": 9,
|
||||
"HostName": "derp9c.tailscale.com",
|
||||
"IPv4": "155.138.243.219",
|
||||
"IPv6": "2001:19f0:6401:fe7:5400:3ff:fe8d:6d9c"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -112,22 +112,36 @@ func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
|
||||
return mtus, err
|
||||
}
|
||||
|
||||
func notTailscaleInterface(iface *winipcfg.IPAdapterAddresses) bool {
|
||||
// TODO(bradfitz): do this without the Description method's
|
||||
// utf16-to-string allocation. But at least we only do it for
|
||||
// the virtual interfaces, for which there won't be many.
|
||||
return !(iface.IfType == winipcfg.IfTypePropVirtual &&
|
||||
iface.Description() == tsconst.WintunInterfaceDesc)
|
||||
}
|
||||
|
||||
// NonTailscaleInterfaces returns a map of interface LUID to interface
|
||||
// for all interfaces except Tailscale tunnels.
|
||||
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces)
|
||||
return getInterfaces(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces, notTailscaleInterface)
|
||||
}
|
||||
|
||||
// getInterfaces returns a map of interfaces keyed by their LUID for
|
||||
// all interfaces matching the provided match predicate.
|
||||
//
|
||||
// The family (AF_UNSPEC, AF_INET, or AF_INET6) and flags are passed
|
||||
// to winipcfg.GetAdaptersAddresses.
|
||||
func getInterfaces(family winipcfg.AddressFamily, flags winipcfg.GAAFlags, match func(*winipcfg.IPAdapterAddresses) bool) (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := winipcfg.GetAdaptersAddresses(family, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
|
||||
for _, iface := range ifs {
|
||||
if iface.Description() == tsconst.WintunInterfaceDesc {
|
||||
continue
|
||||
if match(iface) {
|
||||
ret[iface.LUID] = iface
|
||||
}
|
||||
ret[iface.LUID] = iface
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -135,8 +149,26 @@ func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, e
|
||||
// default route for the given address family.
|
||||
//
|
||||
// It returns (nil, nil) if no interface is found.
|
||||
//
|
||||
// The family must be one of AF_INET or AF_INET6.
|
||||
func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := NonTailscaleInterfaces()
|
||||
ifs, err := getInterfaces(family, winipcfg.GAAFlagIncludeAllInterfaces, func(iface *winipcfg.IPAdapterAddresses) bool {
|
||||
switch iface.IfType {
|
||||
case winipcfg.IfTypeSoftwareLoopback:
|
||||
return false
|
||||
}
|
||||
switch family {
|
||||
case windows.AF_INET:
|
||||
if iface.Flags&winipcfg.IPAAFlagIpv4Enabled == 0 {
|
||||
return false
|
||||
}
|
||||
case windows.AF_INET6:
|
||||
if iface.Flags&winipcfg.IPAAFlagIpv6Enabled == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return iface.OperStatus == winipcfg.IfOperStatusUp && notTailscaleInterface(iface)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,12 +181,31 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres
|
||||
bestMetric := ^uint32(0)
|
||||
var bestIface *winipcfg.IPAdapterAddresses
|
||||
for _, route := range routes {
|
||||
iface := ifs[route.InterfaceLUID]
|
||||
if route.DestinationPrefix.PrefixLength != 0 || iface == nil {
|
||||
if route.DestinationPrefix.PrefixLength != 0 {
|
||||
// Not a default route.
|
||||
continue
|
||||
}
|
||||
if iface.OperStatus == winipcfg.IfOperStatusUp && route.Metric < bestMetric {
|
||||
bestMetric = route.Metric
|
||||
iface := ifs[route.InterfaceLUID]
|
||||
if iface == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Microsoft docs say:
|
||||
//
|
||||
// "The actual route metric used to compute the route
|
||||
// preferences for IPv4 is the summation of the route
|
||||
// metric offset specified in the Metric member of the
|
||||
// MIB_IPFORWARD_ROW2 structure and the interface
|
||||
// metric specified in this member for IPv4"
|
||||
metric := route.Metric
|
||||
switch family {
|
||||
case windows.AF_INET:
|
||||
metric += iface.Ipv4Metric
|
||||
case windows.AF_INET6:
|
||||
metric += iface.Ipv6Metric
|
||||
}
|
||||
if metric < bestMetric {
|
||||
bestMetric = metric
|
||||
bestIface = iface
|
||||
}
|
||||
}
|
||||
@@ -163,6 +214,9 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres
|
||||
}
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
// We always return the IPv4 default route.
|
||||
// TODO(bradfitz): adjust API if/when anything cares. They could in theory differ, though,
|
||||
// in which case we might send traffic to the wrong interface.
|
||||
iface, err := GetWindowsDefault(windows.AF_INET)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -11,8 +11,11 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
|
||||
@@ -21,15 +24,26 @@ type TestIGD struct {
|
||||
upnpConn net.PacketConn // for UPnP discovery
|
||||
pxpConn net.PacketConn // for NAT-PMP and/or PCP
|
||||
ts *httptest.Server
|
||||
logf logger.Logf
|
||||
closed syncs.AtomicBool
|
||||
|
||||
// do* will log which packets are sent, but will not reply to unexpected packets.
|
||||
|
||||
doPMP bool
|
||||
doPCP bool
|
||||
doUPnP bool // TODO: more options for 3 flavors of UPnP services
|
||||
doUPnP bool
|
||||
|
||||
mu sync.Mutex // guards below
|
||||
counters igdCounters
|
||||
}
|
||||
|
||||
// TestIGDOptions are options
|
||||
type TestIGDOptions struct {
|
||||
PMP bool
|
||||
PCP bool
|
||||
UPnP bool // TODO: more options for 3 flavors of UPnP services
|
||||
}
|
||||
|
||||
type igdCounters struct {
|
||||
numUPnPDiscoRecv int32
|
||||
numUPnPOtherUDPRecv int32
|
||||
@@ -38,21 +52,28 @@ type igdCounters struct {
|
||||
numPMPDiscoRecv int32
|
||||
numPCPRecv int32
|
||||
numPCPDiscoRecv int32
|
||||
numPCPMapRecv int32
|
||||
numPCPOtherRecv int32
|
||||
numPMPPublicAddrRecv int32
|
||||
numPMPBogusRecv int32
|
||||
|
||||
numFailedWrites int32
|
||||
invalidPCPMapPkt int32
|
||||
}
|
||||
|
||||
func NewTestIGD() (*TestIGD, error) {
|
||||
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
|
||||
d := &TestIGD{
|
||||
doPMP: true,
|
||||
doPCP: true,
|
||||
doUPnP: true,
|
||||
logf: logf,
|
||||
doPMP: t.PMP,
|
||||
doPCP: t.PCP,
|
||||
doUPnP: t.UPnP,
|
||||
}
|
||||
var err error
|
||||
if d.upnpConn, err = net.ListenPacket("udp", "127.0.0.1:1900"); err != nil {
|
||||
if d.upnpConn, err = testListenUDP(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.pxpConn, err = net.ListenPacket("udp", "127.0.0.1:5351"); err != nil {
|
||||
if d.pxpConn, err = testListenUDP(); err != nil {
|
||||
d.upnpConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
d.ts = httptest.NewServer(http.HandlerFunc(d.serveUPnPHTTP))
|
||||
@@ -61,7 +82,24 @@ func NewTestIGD() (*TestIGD, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func testListenUDP() (net.PacketConn, error) {
|
||||
return net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
}
|
||||
|
||||
func (d *TestIGD) TestPxPPort() uint16 {
|
||||
return uint16(d.pxpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||
}
|
||||
|
||||
func (d *TestIGD) TestUPnPPort() uint16 {
|
||||
return uint16(d.upnpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||
}
|
||||
|
||||
func testIPAndGateway() (gw, ip netaddr.IP, ok bool) {
|
||||
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
||||
}
|
||||
|
||||
func (d *TestIGD) Close() error {
|
||||
d.closed.Set(true)
|
||||
d.ts.Close()
|
||||
d.upnpConn.Close()
|
||||
d.pxpConn.Close()
|
||||
@@ -89,13 +127,21 @@ func (d *TestIGD) serveUPnPDiscovery() {
|
||||
for {
|
||||
n, src, err := d.upnpConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if !d.closed.Get() {
|
||||
d.logf("serveUPnP failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if bytes.Equal(pkt, uPnPPacket) { // a super lazy "parse"
|
||||
d.inc(&d.counters.numUPnPDiscoRecv)
|
||||
resPkt := []byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: %s\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n", d.ts.URL+"/rootDesc.xml"))
|
||||
d.upnpConn.WriteTo(resPkt, src)
|
||||
if d.doUPnP {
|
||||
_, err = d.upnpConn.WriteTo(resPkt, src)
|
||||
if err != nil {
|
||||
d.inc(&d.counters.numFailedWrites)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
d.inc(&d.counters.numUPnPOtherUDPRecv)
|
||||
}
|
||||
@@ -108,6 +154,9 @@ func (d *TestIGD) servePxP() {
|
||||
for {
|
||||
n, a, err := d.pxpConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if !d.closed.Get() {
|
||||
d.logf("servePxP failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ua := a.(*net.UDPAddr)
|
||||
@@ -151,5 +200,55 @@ func (d *TestIGD) handlePMPQuery(pkt []byte, src netaddr.IPPort) {
|
||||
|
||||
func (d *TestIGD) handlePCPQuery(pkt []byte, src netaddr.IPPort) {
|
||||
d.inc(&d.counters.numPCPRecv)
|
||||
// TODO
|
||||
if len(pkt) < 24 {
|
||||
return
|
||||
}
|
||||
op := pkt[1]
|
||||
pktSrcBytes := [16]byte{}
|
||||
copy(pktSrcBytes[:], pkt[8:24])
|
||||
pktSrc := netaddr.IPFrom16(pktSrcBytes)
|
||||
if pktSrc != src.IP() {
|
||||
// TODO this error isn't fatal but should be rejected by server.
|
||||
// Since it's a test it's difficult to get them the same though.
|
||||
d.logf("mismatch of packet source and source IP: got %v, expected %v", pktSrc, src.IP())
|
||||
}
|
||||
switch op {
|
||||
case pcpOpAnnounce:
|
||||
d.inc(&d.counters.numPCPDiscoRecv)
|
||||
if !d.doPCP {
|
||||
return
|
||||
}
|
||||
resp := buildPCPDiscoResponse(pkt)
|
||||
if _, err := d.pxpConn.WriteTo(resp, src.UDPAddr()); err != nil {
|
||||
d.inc(&d.counters.numFailedWrites)
|
||||
}
|
||||
case pcpOpMap:
|
||||
if len(pkt) < 60 {
|
||||
d.logf("got too short packet for pcp op map: %v", pkt)
|
||||
d.inc(&d.counters.invalidPCPMapPkt)
|
||||
return
|
||||
}
|
||||
d.inc(&d.counters.numPCPMapRecv)
|
||||
if !d.doPCP {
|
||||
return
|
||||
}
|
||||
resp := buildPCPMapResponse(pkt)
|
||||
d.pxpConn.WriteTo(resp, src.UDPAddr())
|
||||
default:
|
||||
// unknown op code, ignore it for now.
|
||||
d.inc(&d.counters.numPCPOtherRecv)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T, igd *TestIGD) *Client {
|
||||
var c *Client
|
||||
c = NewClient(t.Logf, func() {
|
||||
t.Logf("port map changed")
|
||||
t.Logf("have mapping: %v", c.HaveMapping())
|
||||
})
|
||||
c.testPxPPort = igd.TestPxPPort()
|
||||
c.testUPnPPort = igd.TestUPnPPort()
|
||||
c.SetGatewayLookupFunc(testIPAndGateway)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
// References:
|
||||
@@ -22,8 +21,8 @@ import (
|
||||
|
||||
// PCP constants
|
||||
const (
|
||||
pcpVersion = 2
|
||||
pcpPort = 5351
|
||||
pcpVersion = 2
|
||||
pcpDefaultPort = 5351
|
||||
|
||||
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
||||
|
||||
@@ -39,7 +38,8 @@ const (
|
||||
)
|
||||
|
||||
type pcpMapping struct {
|
||||
gw netaddr.IP
|
||||
c *Client
|
||||
gw netaddr.IPPort
|
||||
internal netaddr.IPPort
|
||||
external netaddr.IPPort
|
||||
|
||||
@@ -54,13 +54,13 @@ func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
|
||||
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
|
||||
func (p *pcpMapping) External() netaddr.IPPort { return p.external }
|
||||
func (p *pcpMapping) Release(ctx context.Context) {
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
pkt := buildPCPRequestMappingPacket(p.internal.IP(), p.internal.Port(), p.external.Port(), 0, p.external.IP())
|
||||
uc.WriteTo(pkt, netaddr.IPPortFrom(p.gw, pcpPort).UDPAddr())
|
||||
uc.WriteTo(pkt, p.gw.UDPAddr())
|
||||
}
|
||||
|
||||
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
|
||||
@@ -95,6 +95,8 @@ func buildPCPRequestMappingPacket(
|
||||
return pkt
|
||||
}
|
||||
|
||||
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
|
||||
// In particular, its Client is not populated.
|
||||
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||
if len(resp) < 60 {
|
||||
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -25,3 +26,37 @@ func TestParsePCPMapResponse(t *testing.T) {
|
||||
t.Errorf("mismatched external address, got: %v, want: %v", mapping.external, expectedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
serverResponseBit = 1 << 7
|
||||
fakeLifetimeSec = 1<<31 - 1
|
||||
)
|
||||
|
||||
func buildPCPDiscoResponse(req []byte) []byte {
|
||||
out := make([]byte, 24)
|
||||
out[0] = pcpVersion
|
||||
out[1] = req[1] | serverResponseBit
|
||||
out[3] = 0
|
||||
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
|
||||
return out
|
||||
}
|
||||
|
||||
func buildPCPMapResponse(req []byte) []byte {
|
||||
out := make([]byte, 24+36)
|
||||
out[0] = pcpVersion
|
||||
out[1] = req[1] | serverResponseBit
|
||||
out[3] = 0
|
||||
binary.BigEndian.PutUint32(out[4:8], 1<<30)
|
||||
// Do not put an epoch time in 8:12, when we start using it, tests that use it should fail.
|
||||
mapResp := out[24:]
|
||||
mapReq := req[24:]
|
||||
// copy nonce, protocol and internal port
|
||||
copy(mapResp[:13], mapReq[:13])
|
||||
copy(mapResp[16:18], mapReq[16:18])
|
||||
// assign external port
|
||||
binary.BigEndian.PutUint16(mapResp[18:20], 4242)
|
||||
assignedIP := netaddr.IPv4(127, 0, 0, 1)
|
||||
assignedIP16 := assignedIP.As16()
|
||||
copy(mapResp[20:36], assignedIP16[:])
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -55,6 +56,8 @@ type Client struct {
|
||||
logf logger.Logf
|
||||
ipAndGateway func() (gw, ip netaddr.IP, ok bool)
|
||||
onChange func() // or nil
|
||||
testPxPPort uint16 // if non-zero, pxpPort to use for tests
|
||||
testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
|
||||
|
||||
mu sync.Mutex // guards following, and all fields thereof
|
||||
|
||||
@@ -113,7 +116,8 @@ func (c *Client) HaveMapping() bool {
|
||||
//
|
||||
// All fields are immutable once created.
|
||||
type pmpMapping struct {
|
||||
gw netaddr.IP
|
||||
c *Client
|
||||
gw netaddr.IPPort
|
||||
external netaddr.IPPort
|
||||
internal netaddr.IPPort
|
||||
renewAfter time.Time // the time at which we want to renew the mapping
|
||||
@@ -132,13 +136,13 @@ func (p *pmpMapping) External() netaddr.IPPort { return p.external }
|
||||
|
||||
// Release does a best effort fire-and-forget release of the PMP mapping m.
|
||||
func (m *pmpMapping) Release(ctx context.Context) {
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
uc, err := m.c.listenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
|
||||
uc.WriteTo(pkt, netaddr.IPPortFrom(m.gw, pmpPort).UDPAddr())
|
||||
uc.WriteTo(pkt, m.gw.UDPAddr())
|
||||
}
|
||||
|
||||
// NewClient returns a new portmapping client.
|
||||
@@ -213,6 +217,44 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// pxpPort returns the NAT-PMP and PCP port number.
|
||||
// It returns 5351, except for in tests where it varies by run.
|
||||
func (c *Client) pxpPort() uint16 {
|
||||
if c.testPxPPort != 0 {
|
||||
return c.testPxPPort
|
||||
}
|
||||
return pmpDefaultPort
|
||||
}
|
||||
|
||||
// upnpPort returns the UPnP discovery port number.
|
||||
// It returns 1900, except for in tests where it varies by run.
|
||||
func (c *Client) upnpPort() uint16 {
|
||||
if c.testUPnPPort != 0 {
|
||||
return c.testUPnPPort
|
||||
}
|
||||
return upnpDefaultPort
|
||||
}
|
||||
|
||||
func (c *Client) listenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) {
|
||||
// When running under testing conditions, we bind the IGD server
|
||||
// to localhost, and may be running in an environment where our
|
||||
// netns code would decide that binding the portmapper client
|
||||
// socket to the default route interface is the correct way to
|
||||
// ensure connectivity. This can result in us trying to send
|
||||
// packets for 127.0.0.1 out the machine's LAN interface, which
|
||||
// obviously gets dropped on the floor.
|
||||
//
|
||||
// So, under those testing conditions, do _not_ use netns to
|
||||
// create listening sockets. Such sockets are vulnerable to
|
||||
// routing loops, but it's tests that don't set up routing loops,
|
||||
// so we don't care.
|
||||
if c.testPxPPort != 0 || c.testUPnPPort != 0 || os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
var lc net.ListenConfig
|
||||
return lc.ListenPacket(ctx, network, addr)
|
||||
}
|
||||
return netns.Listener().ListenPacket(ctx, network, addr)
|
||||
}
|
||||
|
||||
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
||||
if c.mapping != nil {
|
||||
if releaseOld {
|
||||
@@ -399,7 +441,8 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
// PCP returns all the information necessary for a mapping in a single packet, so we can
|
||||
// construct it upon receiving that packet.
|
||||
m := &pmpMapping{
|
||||
gw: gw,
|
||||
c: c,
|
||||
gw: netaddr.IPPortFrom(gw, c.pxpPort()),
|
||||
internal: internalAddr,
|
||||
}
|
||||
if haveRecentPMP {
|
||||
@@ -415,7 +458,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
uc, err := c.listenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
@@ -424,7 +467,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pxpAddr := netaddr.IPPortFrom(gw, pmpPort)
|
||||
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort())
|
||||
pxpAddru := pxpAddr.UDPAddr()
|
||||
|
||||
preferPCP := !DisablePCP && (DisablePMP || (!haveRecentPMP && haveRecentPCP))
|
||||
@@ -499,8 +542,9 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
// PCP should only have a single packet response
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
pcpMapping.c = c
|
||||
pcpMapping.internal = m.internal
|
||||
pcpMapping.gw = gw
|
||||
pcpMapping.gw = netaddr.IPPortFrom(gw, c.pxpPort())
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.mapping = pcpMapping
|
||||
@@ -524,7 +568,7 @@ type pmpResultCode uint16
|
||||
|
||||
// NAT-PMP constants.
|
||||
const (
|
||||
pmpPort = 5351
|
||||
pmpDefaultPort = 5351
|
||||
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
|
||||
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
|
||||
|
||||
@@ -622,7 +666,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
uc, err := c.listenPacket(context.Background(), "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP: %v", err)
|
||||
return res, err
|
||||
@@ -632,9 +676,8 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
||||
pxpAddr := netaddr.IPPortFrom(gw, c.pxpPort()).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, c.upnpPort()).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
@@ -642,12 +685,12 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else if !DisablePMP {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else if !DisablePCP {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
|
||||
}
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
@@ -669,9 +712,9 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
port := addr.(*net.UDPAddr).Port
|
||||
port := uint16(addr.(*net.UDPAddr).Port)
|
||||
switch port {
|
||||
case upnpPort:
|
||||
case c.upnpPort():
|
||||
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
meta, err := parseUPnPDiscoResponse(buf[:n])
|
||||
if err != nil {
|
||||
@@ -683,10 +726,13 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
c.uPnPSawTime = time.Now()
|
||||
c.uPnPMeta = meta
|
||||
if c.uPnPMeta != meta {
|
||||
c.logf("UPnP meta changed: %+v", meta)
|
||||
c.uPnPMeta = meta
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case pcpPort: // same as pmpPort
|
||||
case c.pxpPort(): // same value for PMP and PCP
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
@@ -729,7 +775,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
|
||||
|
||||
const (
|
||||
upnpPort = 1900 // for UDP discovery only; TCP port discovered later
|
||||
upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
|
||||
)
|
||||
|
||||
// uPnPPacket is the UPnP UDP discovery packet's request body.
|
||||
|
||||
@@ -7,12 +7,10 @@ package portmapper
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestCreateOrGetMapping(t *testing.T) {
|
||||
@@ -60,28 +58,68 @@ func TestClientProbeThenMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProbeIntegration(t *testing.T) {
|
||||
igd, err := NewTestIGD()
|
||||
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: true, PCP: true, UPnP: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer igd.Close()
|
||||
|
||||
logf := t.Logf
|
||||
var c *Client
|
||||
c = NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
|
||||
logf("portmapping changed.")
|
||||
logf("have mapping: %v", c.HaveMapping())
|
||||
})
|
||||
|
||||
c.SetGatewayLookupFunc(func() (gw, self netaddr.IP, ok bool) {
|
||||
return netaddr.IPv4(127, 0, 0, 1), netaddr.IPv4(1, 2, 3, 4), true
|
||||
})
|
||||
c := newTestClient(t, igd)
|
||||
t.Logf("Listening on pxp=%v, upnp=%v", c.testPxPPort, c.testUPnPPort)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Probe(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Probe: %v", err)
|
||||
}
|
||||
if !res.UPnP {
|
||||
t.Errorf("didn't detect UPnP")
|
||||
}
|
||||
st := igd.stats()
|
||||
want := igdCounters{
|
||||
numUPnPDiscoRecv: 1,
|
||||
numPMPRecv: 1,
|
||||
numPCPRecv: 1,
|
||||
numPCPDiscoRecv: 1,
|
||||
numPMPPublicAddrRecv: 1,
|
||||
}
|
||||
if !reflect.DeepEqual(st, want) {
|
||||
t.Errorf("unexpected stats:\n got: %+v\nwant: %+v", st, want)
|
||||
}
|
||||
|
||||
t.Logf("Probe: %+v", res)
|
||||
t.Logf("IGD stats: %+v", igd.stats())
|
||||
t.Logf("IGD stats: %+v", st)
|
||||
// TODO(bradfitz): finish
|
||||
}
|
||||
|
||||
func TestPCPIntegration(t *testing.T) {
|
||||
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PMP: false, PCP: true, UPnP: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer igd.Close()
|
||||
|
||||
c := newTestClient(t, igd)
|
||||
defer c.Close()
|
||||
res, err := c.Probe(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("probe failed: %v", err)
|
||||
}
|
||||
if res.UPnP || res.PMP {
|
||||
t.Errorf("probe unexpectedly saw upnp or pmp: %+v", res)
|
||||
}
|
||||
if !res.PCP {
|
||||
t.Fatalf("probe did not see pcp: %+v", res)
|
||||
}
|
||||
|
||||
external, err := c.createOrGetMapping(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get mapping: %v", err)
|
||||
}
|
||||
if external.IsZero() {
|
||||
t.Errorf("got zero IP, expected non-zero")
|
||||
}
|
||||
if c.mapping == nil {
|
||||
t.Errorf("got nil mapping after successful createOrGetMapping")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,11 @@ func (c *Client) getUPnPPortMapping(
|
||||
|
||||
type uPnPDiscoResponse struct {
|
||||
Location string
|
||||
// Server describes what version the UPnP is, such as MiniUPnPd/2.x.x
|
||||
Server string
|
||||
// USN is the serial number of the device, which also contains
|
||||
// what kind of UPnP service is being offered, i.e. InternetGatewayDevice:2
|
||||
USN string
|
||||
}
|
||||
|
||||
// parseUPnPDiscoResponse parses a UPnP HTTP-over-UDP discovery response.
|
||||
@@ -321,5 +326,7 @@ func parseUPnPDiscoResponse(body []byte) (uPnPDiscoResponse, error) {
|
||||
return r, err
|
||||
}
|
||||
r.Location = res.Header.Get("Location")
|
||||
r.Server = res.Header.Get("Server")
|
||||
r.USN = res.Header.Get("Usn")
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -43,9 +43,13 @@ func TestParseUPnPDiscoResponse(t *testing.T) {
|
||||
}{
|
||||
{"google", googleWifiUPnPDisco, uPnPDiscoResponse{
|
||||
Location: "http://192.168.86.1:5000/rootDesc.xml",
|
||||
Server: "Linux/5.4.0-1034-gcp UPnP/1.1 MiniUPnPd/1.9",
|
||||
USN: "uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
||||
}},
|
||||
{"pfsense", pfSenseUPnPDisco, uPnPDiscoResponse{
|
||||
Location: "http://192.168.1.1:2189/rootDesc.xml",
|
||||
Server: "FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1",
|
||||
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"inet.af/netaddr"
|
||||
"inet.af/netstack/tcpip"
|
||||
@@ -32,25 +31,30 @@ var ourMAC = net.HardwareAddr{0x30, 0x2D, 0x66, 0xEC, 0x7A, 0x93}
|
||||
|
||||
func init() { createTAP = createTAPLinux }
|
||||
|
||||
func createTAPLinux(tapName, bridgeName string) (dev tun.Device, err error) {
|
||||
fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR, 0)
|
||||
func createTAPLinux(tapName, bridgeName string) (tun.Device, error) {
|
||||
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ifr struct {
|
||||
name [16]byte
|
||||
flags uint16
|
||||
_ [22]byte
|
||||
|
||||
dev, err := openDevice(fd, tapName, bridgeName)
|
||||
if err != nil {
|
||||
unix.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
copy(ifr.name[:], tapName)
|
||||
ifr.flags = syscall.IFF_TAP | syscall.IFF_NO_PI
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr)))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return nil, errno
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
func openDevice(fd int, tapName, bridgeName string) (tun.Device, error) {
|
||||
ifr, err := unix.NewIfreq(tapName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = syscall.SetNonblock(fd, true); err != nil {
|
||||
syscall.Close(fd)
|
||||
|
||||
// Flags are stored as a uint16 in the ifreq union.
|
||||
ifr.SetUint16(unix.IFF_TAP | unix.IFF_NO_PI)
|
||||
if err := unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -62,11 +66,13 @@ func createTAPLinux(tapName, bridgeName string) (dev tun.Device, err error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
dev, _, err = tun.CreateUnmonitoredTUNFromFD(fd) // TODO: MTU
|
||||
|
||||
// Also sets non-blocking I/O on fd when creating tun.Device.
|
||||
dev, _, err := tun.CreateUnmonitoredTUNFromFD(fd) // TODO: MTU
|
||||
if err != nil {
|
||||
syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
package tstun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -18,7 +16,6 @@ import (
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// tunMTU is the MTU we set on tailscale's TUN interface. wireguard-go
|
||||
@@ -78,90 +75,18 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
|
||||
return dev, name, nil
|
||||
}
|
||||
|
||||
// tunDiagnoseFailure, if non-nil, does OS-specific diagnostics of why
|
||||
// TUN failed to work.
|
||||
var tunDiagnoseFailure func(tunName string, logf logger.Logf)
|
||||
|
||||
// Diagnose tries to explain a tuntap device creation failure.
|
||||
// It pokes around the system and logs some diagnostic info that might
|
||||
// help debug why tun creation failed. Because device creation has
|
||||
// already failed and the program's about to end, log a lot.
|
||||
func Diagnose(logf logger.Logf, tunName string) {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
diagnoseLinuxTUNFailure(tunName, logf)
|
||||
case "darwin":
|
||||
diagnoseDarwinTUNFailure(tunName, logf)
|
||||
default:
|
||||
if tunDiagnoseFailure != nil {
|
||||
tunDiagnoseFailure(tunName, logf)
|
||||
} else {
|
||||
logf("no TUN failure diagnostics for OS %q", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func diagnoseDarwinTUNFailure(tunName string, logf logger.Logf) {
|
||||
if os.Getuid() != 0 {
|
||||
logf("failed to create TUN device as non-root user; use 'sudo tailscaled', or run under launchd with 'sudo tailscaled install-system-daemon'")
|
||||
}
|
||||
if tunName != "utun" {
|
||||
logf("failed to create TUN device %q; try using tun device \"utun\" instead for automatic selection", tunName)
|
||||
}
|
||||
}
|
||||
|
||||
func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf) {
|
||||
kernel, err := exec.Command("uname", "-r").Output()
|
||||
kernel = bytes.TrimSpace(kernel)
|
||||
if err != nil {
|
||||
logf("no TUN, and failed to look up kernel version: %v", err)
|
||||
return
|
||||
}
|
||||
logf("Linux kernel version: %s", kernel)
|
||||
|
||||
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput()
|
||||
if err == nil {
|
||||
logf("'modprobe tun' successful")
|
||||
// Either tun is currently loaded, or it's statically
|
||||
// compiled into the kernel (which modprobe checks
|
||||
// with /lib/modules/$(uname -r)/modules.builtin)
|
||||
//
|
||||
// So if there's a problem at this point, it's
|
||||
// probably because /dev/net/tun doesn't exist.
|
||||
const dev = "/dev/net/tun"
|
||||
if fi, err := os.Stat(dev); err != nil {
|
||||
logf("tun module loaded in kernel, but %s does not exist", dev)
|
||||
} else {
|
||||
logf("%s: %v", dev, fi.Mode())
|
||||
}
|
||||
|
||||
// We failed to find why it failed. Just let our
|
||||
// caller report the error it got from wireguard-go.
|
||||
return
|
||||
}
|
||||
logf("is CONFIG_TUN enabled in your kernel? `modprobe tun` failed with: %s", modprobeOut)
|
||||
|
||||
switch distro.Get() {
|
||||
case distro.Debian:
|
||||
dpkgOut, err := exec.Command("dpkg", "-S", "kernel/drivers/net/tun.ko").CombinedOutput()
|
||||
if len(bytes.TrimSpace(dpkgOut)) == 0 || err != nil {
|
||||
logf("tun module not loaded nor found on disk")
|
||||
return
|
||||
}
|
||||
if !bytes.Contains(dpkgOut, kernel) {
|
||||
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", dpkgOut)
|
||||
}
|
||||
case distro.Arch:
|
||||
findOut, err := exec.Command("find", "/lib/modules/", "-path", "*/net/tun.ko*").CombinedOutput()
|
||||
if len(bytes.TrimSpace(findOut)) == 0 || err != nil {
|
||||
logf("tun module not loaded nor found on disk")
|
||||
return
|
||||
}
|
||||
if !bytes.Contains(findOut, kernel) {
|
||||
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut)
|
||||
}
|
||||
case distro.OpenWrt:
|
||||
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("error querying OpenWrt installed packages: %s", out)
|
||||
return
|
||||
}
|
||||
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
|
||||
if !bytes.Contains(out, []byte(pkg+" - ")) {
|
||||
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
net/tstun/tun_linux.go
Normal file
96
net/tstun/tun_linux.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2021 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 tstun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tunDiagnoseFailure = diagnoseLinuxTUNFailure
|
||||
}
|
||||
|
||||
func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf) {
|
||||
var un syscall.Utsname
|
||||
err := syscall.Uname(&un)
|
||||
if err != nil {
|
||||
logf("no TUN, and failed to look up kernel version: %v", err)
|
||||
return
|
||||
}
|
||||
kernel := utsReleaseField(&un)
|
||||
logf("Linux kernel version: %s", kernel)
|
||||
|
||||
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput()
|
||||
if err == nil {
|
||||
logf("'modprobe tun' successful")
|
||||
// Either tun is currently loaded, or it's statically
|
||||
// compiled into the kernel (which modprobe checks
|
||||
// with /lib/modules/$(uname -r)/modules.builtin)
|
||||
//
|
||||
// So if there's a problem at this point, it's
|
||||
// probably because /dev/net/tun doesn't exist.
|
||||
const dev = "/dev/net/tun"
|
||||
if fi, err := os.Stat(dev); err != nil {
|
||||
logf("tun module loaded in kernel, but %s does not exist", dev)
|
||||
} else {
|
||||
logf("%s: %v", dev, fi.Mode())
|
||||
}
|
||||
|
||||
// We failed to find why it failed. Just let our
|
||||
// caller report the error it got from wireguard-go.
|
||||
return
|
||||
}
|
||||
logf("is CONFIG_TUN enabled in your kernel? `modprobe tun` failed with: %s", modprobeOut)
|
||||
|
||||
switch distro.Get() {
|
||||
case distro.Debian:
|
||||
dpkgOut, err := exec.Command("dpkg", "-S", "kernel/drivers/net/tun.ko").CombinedOutput()
|
||||
if len(bytes.TrimSpace(dpkgOut)) == 0 || err != nil {
|
||||
logf("tun module not loaded nor found on disk")
|
||||
return
|
||||
}
|
||||
if !bytes.Contains(dpkgOut, []byte(kernel)) {
|
||||
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", dpkgOut)
|
||||
}
|
||||
case distro.Arch:
|
||||
findOut, err := exec.Command("find", "/lib/modules/", "-path", "*/net/tun.ko*").CombinedOutput()
|
||||
if len(bytes.TrimSpace(findOut)) == 0 || err != nil {
|
||||
logf("tun module not loaded nor found on disk")
|
||||
return
|
||||
}
|
||||
if !bytes.Contains(findOut, []byte(kernel)) {
|
||||
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut)
|
||||
}
|
||||
case distro.OpenWrt:
|
||||
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("error querying OpenWrt installed packages: %s", out)
|
||||
return
|
||||
}
|
||||
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
|
||||
if !bytes.Contains(out, []byte(pkg+" - ")) {
|
||||
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func utsReleaseField(u *syscall.Utsname) string {
|
||||
var sb strings.Builder
|
||||
for _, v := range u.Release {
|
||||
if v == 0 {
|
||||
break
|
||||
}
|
||||
sb.WriteByte(byte(v))
|
||||
}
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
27
net/tstun/tun_macos.go
Normal file
27
net/tstun/tun_macos.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
//go:build darwin && !ios
|
||||
// +build darwin,!ios
|
||||
|
||||
package tstun
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tunDiagnoseFailure = diagnoseDarwinTUNFailure
|
||||
}
|
||||
|
||||
func diagnoseDarwinTUNFailure(tunName string, logf logger.Logf) {
|
||||
if os.Getuid() != 0 {
|
||||
logf("failed to create TUN device as non-root user; use 'sudo tailscaled', or run under launchd with 'sudo tailscaled install-system-daemon'")
|
||||
}
|
||||
if tunName != "utun" {
|
||||
logf("failed to create TUN device %q; try using tun device \"utun\" instead for automatic selection", tunName)
|
||||
}
|
||||
}
|
||||
59
paths/migrate.go
Normal file
59
paths/migrate.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2021 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 paths
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// TryConfigFileMigration carefully copies the contents of oldFile to
|
||||
// newFile, returning the path which should be used to read the config.
|
||||
// - if newFile already exists, don't modify it just return its path
|
||||
// - if neither oldFile nor newFile exist, return newFile for a fresh
|
||||
// default config to be written to.
|
||||
// - if oldFile exists but copying to newFile fails, return oldFile so
|
||||
// there will at least be some config to work with.
|
||||
func TryConfigFileMigration(logf logger.Logf, oldFile, newFile string) string {
|
||||
_, err := os.Stat(newFile)
|
||||
if err == nil {
|
||||
// Common case for a system which has already been migrated.
|
||||
return newFile
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
logf("TryConfigFileMigration failed; new file: %v", err)
|
||||
return newFile
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(oldFile)
|
||||
if err != nil {
|
||||
// Common case for a new user.
|
||||
return newFile
|
||||
}
|
||||
|
||||
if err = MkStateDir(filepath.Dir(newFile)); err != nil {
|
||||
logf("TryConfigFileMigration failed; MkStateDir: %v", err)
|
||||
return oldFile
|
||||
}
|
||||
|
||||
err = os.WriteFile(newFile, contents, 0600)
|
||||
if err != nil {
|
||||
removeErr := os.Remove(newFile)
|
||||
if removeErr != nil {
|
||||
logf("TryConfigFileMigration failed; write newFile no cleanup: %v, remove err: %v",
|
||||
err, removeErr)
|
||||
return oldFile
|
||||
}
|
||||
logf("TryConfigFileMigration failed; write newFile: %v", err)
|
||||
return oldFile
|
||||
}
|
||||
|
||||
logf("TryConfigFileMigration: successfully migrated: from %v to %v",
|
||||
oldFile, newFile)
|
||||
|
||||
return newFile
|
||||
}
|
||||
@@ -55,7 +55,18 @@ func DefaultTailscaledStateFile() string {
|
||||
return f()
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf")
|
||||
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "server-state.conf")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MkStateDir ensures that dirPath, the daemon's configurtaion directory
|
||||
// containing machine keys etc, both exists and has the correct permissions.
|
||||
// We want it to only be accessible to the user the daemon is running under.
|
||||
func MkStateDir(dirPath string) error {
|
||||
if err := os.MkdirAll(dirPath, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ensureStateDirPerms(dirPath)
|
||||
}
|
||||
|
||||
@@ -61,3 +61,11 @@ func xdgDataHome() string {
|
||||
}
|
||||
return filepath.Join(os.Getenv("HOME"), ".local/share")
|
||||
}
|
||||
|
||||
func ensureStateDirPerms(dirPath string) error {
|
||||
if filepath.Base(dirPath) != "tailscale" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.Chmod(dirPath, 0700)
|
||||
}
|
||||
|
||||
150
paths/paths_windows.go
Normal file
150
paths/paths_windows.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2021 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 paths
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func getTokenInfo(token windows.Token, infoClass uint32) ([]byte, error) {
|
||||
var desiredLen uint32
|
||||
err := windows.GetTokenInformation(token, infoClass, nil, 0, &desiredLen)
|
||||
if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := make([]byte, desiredLen)
|
||||
actualLen := desiredLen
|
||||
err = windows.GetTokenInformation(token, infoClass, &buf[0], desiredLen, &actualLen)
|
||||
return buf, err
|
||||
}
|
||||
|
||||
func getTokenUserInfo(token windows.Token) (*windows.Tokenuser, error) {
|
||||
buf, err := getTokenInfo(token, windows.TokenUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*windows.Tokenuser)(unsafe.Pointer(&buf[0])), nil
|
||||
}
|
||||
|
||||
func getTokenPrimaryGroupInfo(token windows.Token) (*windows.Tokenprimarygroup, error) {
|
||||
buf, err := getTokenInfo(token, windows.TokenPrimaryGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*windows.Tokenprimarygroup)(unsafe.Pointer(&buf[0])), nil
|
||||
}
|
||||
|
||||
type userSids struct {
|
||||
User *windows.SID
|
||||
PrimaryGroup *windows.SID
|
||||
}
|
||||
|
||||
func getCurrentUserSids() (*userSids, error) {
|
||||
token, err := windows.OpenCurrentProcessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer token.Close()
|
||||
|
||||
userInfo, err := getTokenUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primaryGroup, err := getTokenPrimaryGroupInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userSids{userInfo.User.Sid, primaryGroup.PrimaryGroup}, nil
|
||||
}
|
||||
|
||||
// ensureStateDirPerms applies a restrictive ACL to the directory specified by dirPath.
|
||||
// It sets the following security attributes on the directory:
|
||||
// Owner: The user for the current process;
|
||||
// Primary Group: The primary group for the current process;
|
||||
// DACL: Full control to the current user and to the Administrators group.
|
||||
// (We include Administrators so that admin users may still access logs;
|
||||
// granting access exclusively to LocalSystem would require admins to use
|
||||
// special tools to access the Log directory)
|
||||
// Inheritance: The directory does not inherit the ACL from its parent.
|
||||
// However, any directories and/or files created within this
|
||||
// directory *do* inherit the ACL that we are setting.
|
||||
func ensureStateDirPerms(dirPath string) error {
|
||||
fi, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if strings.ToLower(filepath.Base(dirPath)) != "tailscale" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We need the info for our current user as SIDs
|
||||
sids, err := getCurrentUserSids()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We also need the SID for the Administrators group so that admins may
|
||||
// easily access logs.
|
||||
adminGroupSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Munge the SIDs into the format required by EXPLICIT_ACCESS.
|
||||
userTrustee := windows.TRUSTEE{nil, windows.NO_MULTIPLE_TRUSTEE,
|
||||
windows.TRUSTEE_IS_SID, windows.TRUSTEE_IS_USER,
|
||||
windows.TrusteeValueFromSID(sids.User)}
|
||||
|
||||
adminTrustee := windows.TRUSTEE{nil, windows.NO_MULTIPLE_TRUSTEE,
|
||||
windows.TRUSTEE_IS_SID, windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
|
||||
windows.TrusteeValueFromSID(adminGroupSid)}
|
||||
|
||||
// We declare our access rights via this array of EXPLICIT_ACCESS structures.
|
||||
// We set full access to our user and to Administrators.
|
||||
// We configure the DACL such that any files or directories created within
|
||||
// dirPath will also inherit this DACL.
|
||||
explicitAccess := []windows.EXPLICIT_ACCESS{
|
||||
windows.EXPLICIT_ACCESS{
|
||||
windows.GENERIC_ALL,
|
||||
windows.SET_ACCESS,
|
||||
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
userTrustee,
|
||||
},
|
||||
windows.EXPLICIT_ACCESS{
|
||||
windows.GENERIC_ALL,
|
||||
windows.SET_ACCESS,
|
||||
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
adminTrustee,
|
||||
},
|
||||
}
|
||||
|
||||
dacl, err := windows.ACLFromEntries(explicitAccess, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We now reset the file's owner, primary group, and DACL.
|
||||
// We also must pass PROTECTED_DACL_SECURITY_INFORMATION so that our new ACL
|
||||
// does not inherit any ACL entries from the parent directory.
|
||||
const flags = windows.OWNER_SECURITY_INFORMATION |
|
||||
windows.GROUP_SECURITY_INFORMATION |
|
||||
windows.DACL_SECURITY_INFORMATION |
|
||||
windows.PROTECTED_DACL_SECURITY_INFORMATION
|
||||
return windows.SetNamedSecurityInfo(dirPath, windows.SE_FILE_OBJECT, flags,
|
||||
sids.User, sids.PrimaryGroup, dacl, nil)
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (go1.16 && !ios) || (!go1.16 && !darwin) || (!go1.16 && !arm64)
|
||||
// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64
|
||||
//go:build !ios
|
||||
// +build !ios
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (windows || freebsd || openbsd || (darwin && go1.16) || (darwin && !go1.16 && !arm64)) && !ios
|
||||
// +build windows freebsd openbsd darwin,go1.16 darwin,!go1.16,!arm64
|
||||
//go:build (windows || freebsd || openbsd || darwin) && !ios
|
||||
// +build windows freebsd openbsd darwin
|
||||
// +build !ios
|
||||
|
||||
package portlist
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (go1.16 && ios) || (!go1.16 && darwin && !amd64)
|
||||
// +build go1.16,ios !go1.16,darwin,!amd64
|
||||
//go:build ios
|
||||
// +build ios
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ((darwin && amd64 && !go1.16) || (darwin && go1.16)) && !ios
|
||||
// +build darwin,amd64,!go1.16 darwin,go1.16
|
||||
// +build !ios
|
||||
//go:build darwin && !ios
|
||||
// +build darwin,!ios
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type closeable interface {
|
||||
@@ -29,9 +30,39 @@ func ConnCloseWrite(c net.Conn) error {
|
||||
return c.(closeable).CloseWrite()
|
||||
}
|
||||
|
||||
var processStartTime = time.Now()
|
||||
var tailscaledProcExists = func() bool { return false } // set by safesocket_ps.go
|
||||
|
||||
// tailscaledStillStarting reports whether tailscaled is probably
|
||||
// still starting up. That is, it reports whether the caller should
|
||||
// keep retrying to connect.
|
||||
func tailscaledStillStarting() bool {
|
||||
d := time.Since(processStartTime)
|
||||
if d < 2*time.Second {
|
||||
// Without even checking the process table, assume
|
||||
// that for the first two seconds that tailscaled is
|
||||
// probably still starting. That is, assume they're
|
||||
// running "tailscaled & tailscale up ...." and make
|
||||
// the tailscale client block for a bit for tailscaled
|
||||
// to start accepting on the socket.
|
||||
return true
|
||||
}
|
||||
if d > 5*time.Second {
|
||||
return false
|
||||
}
|
||||
return tailscaledProcExists()
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
return connect(path, port)
|
||||
for {
|
||||
c, err := connect(path, port)
|
||||
if err != nil && tailscaledStillStarting() {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
|
||||
// Listen returns a listener either on Unix socket path (on Unix), or
|
||||
|
||||
37
safesocket/safesocket_ps.go
Normal file
37
safesocket/safesocket_ps.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
//go:build linux || windows || darwin || freebsd
|
||||
// +build linux windows darwin freebsd
|
||||
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ps "github.com/mitchellh/go-ps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tailscaledProcExists = func() bool {
|
||||
procs, err := ps.Processes()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, proc := range procs {
|
||||
name := proc.Executable()
|
||||
const tailscaled = "tailscaled"
|
||||
if len(name) < len(tailscaled) {
|
||||
continue
|
||||
}
|
||||
// Do case insensitive comparison for Windows,
|
||||
// notably, and ignore any ".exe" suffix.
|
||||
if strings.EqualFold(name[:len(tailscaled)], tailscaled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13 && !go1.16
|
||||
// +build go1.13,!go1.16
|
||||
//go:build go1.13 && !go1.18
|
||||
// +build go1.13,!go1.18
|
||||
|
||||
// This file makes assumptions about the inner workings of sync.Mutex and sync.RWMutex.
|
||||
// This includes not just their memory layout but their invariants and functionality.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13 && !go1.16
|
||||
// +build go1.13,!go1.16
|
||||
//go:build go1.13 && !go1.18
|
||||
// +build go1.13,!go1.18
|
||||
|
||||
package syncs
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ import (
|
||||
// 20: 2021-06-11: MapResponse.LastSeen used even less (https://github.com/tailscale/tailscale/issues/2107)
|
||||
// 21: 2021-06-15: added MapResponse.DNSConfig.CertDomains
|
||||
// 22: 2021-06-16: added MapResponse.DNSConfig.ExtraRecords
|
||||
const CurrentMapRequestVersion = 22
|
||||
// 23: 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
|
||||
const CurrentMapRequestVersion = 23
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -407,7 +408,7 @@ type Hostinfo struct {
|
||||
OS string // operating system the client runs on (a version.OS value)
|
||||
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
Package string `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
|
||||
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
|
||||
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")
|
||||
Hostname string // name of the host the client runs on
|
||||
ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections
|
||||
ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user
|
||||
@@ -837,12 +838,19 @@ var FilterAllowAll = []FilterRule{
|
||||
type DNSConfig struct {
|
||||
// Resolvers are the DNS resolvers to use, in order of preference.
|
||||
Resolvers []dnstype.Resolver `json:",omitempty"`
|
||||
|
||||
// Routes maps DNS name suffixes to a set of DNS resolvers to
|
||||
// use. It is used to implement "split DNS" and other advanced DNS
|
||||
// routing overlays.
|
||||
// Map keys must be fully-qualified DNS name suffixes, with a
|
||||
// trailing dot but no leading dot.
|
||||
//
|
||||
// Map keys are fully-qualified DNS name suffixes; they may
|
||||
// optionally contain a trailing dot but no leading dot.
|
||||
//
|
||||
// If the value is an empty slice, that means the suffix should still
|
||||
// be handled by Tailscale's built-in resolver (100.100.100.100), such
|
||||
// as for the purpose of handling ExtraRecords.
|
||||
Routes map[string][]dnstype.Resolver `json:",omitempty"`
|
||||
|
||||
// FallbackResolvers is like Resolvers, but is only used if a
|
||||
// split DNS configuration is requested in a configuration that
|
||||
// doesn't work yet without explicit default resolvers.
|
||||
@@ -897,19 +905,30 @@ type DNSRecord struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
// PingRequest is a request to send an HTTP request to prove the
|
||||
// PingRequest with no IP and Types is a request to send an HTTP request to prove the
|
||||
// long-polling client is still connected.
|
||||
// PingRequest with Types and IP, will send a ping to the IP and send a
|
||||
// POST request to the URL to prove that the ping succeeded.
|
||||
type PingRequest struct {
|
||||
// URL is the URL to send a HEAD request to.
|
||||
// It will be a unique URL each time. No auth headers are necessary.
|
||||
//
|
||||
// If the client sees multiple PingRequests with the same URL,
|
||||
// subsequent ones should be ignored.
|
||||
// If Types and IP are defined, then URL is the URL to send a POST request to.
|
||||
URL string
|
||||
|
||||
// Log is whether to log about this ping in the success case.
|
||||
// For failure cases, the client will log regardless.
|
||||
Log bool `json:",omitempty"`
|
||||
|
||||
// Types is the types of ping that is initiated. Can be TSMP, ICMP or disco.
|
||||
// Types will be comma separated, such as TSMP,disco.
|
||||
Types string
|
||||
|
||||
// IP is the ping target.
|
||||
// It is used in TSMP pings, if IP is invalid or empty then do a HEAD request to the URL.
|
||||
IP netaddr.IP
|
||||
}
|
||||
|
||||
type MapResponse struct {
|
||||
|
||||
@@ -177,11 +177,12 @@ func (s *Server) start() error {
|
||||
err = lb.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
UpdatePrefs: prefs,
|
||||
AuthKey: os.Getenv("TS_AUTHKEY"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting backend: %w", err)
|
||||
}
|
||||
if os.Getenv("TS_LOGIN") == "1" {
|
||||
if os.Getenv("TS_LOGIN") == "1" || os.Getenv("TS_AUTHKEY") != "" {
|
||||
s.lb.StartLoginInteractive()
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
@@ -269,6 +270,33 @@ func (s *Server) nodeLocked(nodeKey tailcfg.NodeKey) *tailcfg.Node {
|
||||
return s.nodes[nodeKey].Clone()
|
||||
}
|
||||
|
||||
// AddFakeNode injects a fake node into the server.
|
||||
func (s *Server) AddFakeNode() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.nodes == nil {
|
||||
s.nodes = make(map[tailcfg.NodeKey]*tailcfg.Node)
|
||||
}
|
||||
nk := tailcfg.NodeKey(key.NewPrivate().Public())
|
||||
mk := tailcfg.MachineKey(key.NewPrivate().Public())
|
||||
dk := tailcfg.DiscoKey(key.NewPrivate().Public())
|
||||
id := int64(binary.LittleEndian.Uint64(nk[:]))
|
||||
ip := netaddr.IPv4(nk[0], nk[1], nk[2], nk[3])
|
||||
addr := netaddr.IPPrefixFrom(ip, 32)
|
||||
s.nodes[nk] = &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(id),
|
||||
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", id)),
|
||||
User: tailcfg.UserID(id),
|
||||
Machine: mk,
|
||||
Key: nk,
|
||||
MachineAuthorized: true,
|
||||
DiscoKey: dk,
|
||||
Addresses: []netaddr.IPPrefix{addr},
|
||||
AllowedIPs: []netaddr.IPPrefix{addr},
|
||||
}
|
||||
// TODO: send updates to other (non-fake?) nodes
|
||||
}
|
||||
|
||||
func (s *Server) AllNodes() (nodes []*tailcfg.Node) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -661,6 +689,9 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
res.Peers = append(res.Peers, p)
|
||||
}
|
||||
}
|
||||
sort.Slice(res.Peers, func(i, j int) bool {
|
||||
return res.Peers[i].ID < res.Peers[j].ID
|
||||
})
|
||||
|
||||
v4Prefix := netaddr.IPPrefixFrom(netaddr.IPv4(100, 64, uint8(tailcfg.NodeID(user.ID)>>8), uint8(tailcfg.NodeID(user.ID))), 32)
|
||||
v6Prefix := netaddr.IPPrefixFrom(tsaddr.Tailscale4To6(v4Prefix.IP()), 128)
|
||||
|
||||
@@ -182,15 +182,18 @@ func TestLongRunningQPS(t *testing.T) {
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
// This will still offer ~500 requests per second,
|
||||
// but won't consume outrageous amount of CPU.
|
||||
start := time.Now()
|
||||
end := start.Add(5 * time.Second)
|
||||
for time.Now().Before(end) {
|
||||
ticker := time.NewTicker(2 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for now := range ticker.C {
|
||||
if now.After(end) {
|
||||
break
|
||||
}
|
||||
wg.Add(1)
|
||||
go f()
|
||||
|
||||
// This will still offer ~500 requests per second, but won't consume
|
||||
// outrageous amount of CPU.
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
wg.Wait()
|
||||
elapsed := time.Since(start)
|
||||
@@ -201,7 +204,7 @@ func TestLongRunningQPS(t *testing.T) {
|
||||
t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal)
|
||||
}
|
||||
// We should get very close to the number of requests allowed.
|
||||
if want := int32(0.999 * ideal); numOK < want {
|
||||
if want := int32(0.995 * ideal); numOK < want {
|
||||
t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal)
|
||||
}
|
||||
}
|
||||
|
||||
257
tsweb/tsweb.go
257
tsweb/tsweb.go
@@ -19,6 +19,7 @@ import (
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -27,9 +28,12 @@ import (
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
expvar.Publish("process_start_unix_time", expvar.Func(func() interface{} { return timeStart.Unix() }))
|
||||
expvar.Publish("version", expvar.Func(func() interface{} { return version.Long }))
|
||||
expvar.Publish("counter_uptime_sec", expvar.Func(func() interface{} { return int64(Uptime().Seconds()) }))
|
||||
expvar.Publish("gauge_goroutines", expvar.Func(func() interface{} { return runtime.NumGoroutine() }))
|
||||
}
|
||||
@@ -342,6 +346,141 @@ func Error(code int, msg string, err error) HTTPError {
|
||||
return HTTPError{Code: code, Msg: msg, Err: err}
|
||||
}
|
||||
|
||||
// WritePrometheusExpvar writes kv to w in Prometheus metrics format.
|
||||
//
|
||||
// See VarzHandler for conventions. This is exported primarily for
|
||||
// people to test their varz.
|
||||
func WritePrometheusExpvar(w io.Writer, kv expvar.KeyValue) {
|
||||
writePromExpVar(w, "", kv)
|
||||
}
|
||||
|
||||
func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
|
||||
key := kv.Key
|
||||
var typ string
|
||||
var label string
|
||||
switch {
|
||||
case strings.HasPrefix(kv.Key, "gauge_"):
|
||||
typ = "gauge"
|
||||
key = strings.TrimPrefix(kv.Key, "gauge_")
|
||||
|
||||
case strings.HasPrefix(kv.Key, "counter_"):
|
||||
typ = "counter"
|
||||
key = strings.TrimPrefix(kv.Key, "counter_")
|
||||
}
|
||||
if strings.HasPrefix(key, "labelmap_") {
|
||||
key = strings.TrimPrefix(key, "labelmap_")
|
||||
if i := strings.Index(key, "_"); i != -1 {
|
||||
label, key = key[:i], key[i+1:]
|
||||
}
|
||||
}
|
||||
name := prefix + key
|
||||
|
||||
switch v := kv.Value.(type) {
|
||||
case *expvar.Int:
|
||||
if typ == "" {
|
||||
typ = "counter"
|
||||
}
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, v.Value())
|
||||
return
|
||||
case *metrics.Set:
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
writePromExpVar(w, name+"_", kv)
|
||||
})
|
||||
return
|
||||
case PrometheusMetricsReflectRooter:
|
||||
root := v.PrometheusMetricsReflectRoot()
|
||||
rv := reflect.ValueOf(root)
|
||||
if rv.Type().Kind() == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
return
|
||||
}
|
||||
rv = rv.Elem()
|
||||
}
|
||||
if rv.Type().Kind() != reflect.Struct {
|
||||
fmt.Fprintf(w, "# skipping expvar %q; unknown root type\n", name)
|
||||
return
|
||||
}
|
||||
foreachExportedStructField(rv, func(fieldOrJSONName, metricType string, rv reflect.Value) {
|
||||
mname := name + "_" + fieldOrJSONName
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Float())
|
||||
case reflect.Struct:
|
||||
if rv.CanAddr() {
|
||||
// Slight optimization, not copying big structs if they're addressable:
|
||||
writePromExpVar(w, name+"_", expvar.KeyValue{Key: fieldOrJSONName, Value: expVarPromStructRoot{rv.Addr().Interface()}})
|
||||
} else {
|
||||
writePromExpVar(w, name+"_", expvar.KeyValue{Key: fieldOrJSONName, Value: expVarPromStructRoot{rv.Interface()}})
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if typ == "" {
|
||||
var funcRet string
|
||||
if f, ok := kv.Value.(expvar.Func); ok {
|
||||
v := f()
|
||||
if ms, ok := v.(runtime.MemStats); ok && name == "memstats" {
|
||||
writeMemstats(w, &ms)
|
||||
return
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64:
|
||||
fmt.Fprintf(w, "%s %v\n", name, v)
|
||||
return
|
||||
}
|
||||
funcRet = fmt.Sprintf(" returning %T", v)
|
||||
}
|
||||
switch kv.Value.(type) {
|
||||
default:
|
||||
fmt.Fprintf(w, "# skipping expvar %q (Go type %T%s) with undeclared Prometheus type\n", name, kv.Value, funcRet)
|
||||
return
|
||||
case *metrics.LabelMap, *expvar.Map:
|
||||
// Permit typeless LabelMap and expvar.Map for
|
||||
// compatibility with old expvar-registered
|
||||
// metrics.LabelMap.
|
||||
}
|
||||
}
|
||||
|
||||
switch v := kv.Value.(type) {
|
||||
case expvar.Func:
|
||||
val := v()
|
||||
switch val.(type) {
|
||||
case float64, int64, int:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, val)
|
||||
default:
|
||||
fmt.Fprintf(w, "# skipping expvar func %q returning unknown type %T\n", name, val)
|
||||
}
|
||||
|
||||
case *metrics.LabelMap:
|
||||
if typ != "" {
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
|
||||
}
|
||||
// IntMap uses expvar.Map on the inside, which presorts
|
||||
// keys. The output ordering is deterministic.
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, v.Label, kv.Key, kv.Value)
|
||||
})
|
||||
case *expvar.Map:
|
||||
if label != "" && typ != "" {
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, label, kv.Key, kv.Value)
|
||||
})
|
||||
} else {
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
fmt.Fprintf(w, "%s_%s %v\n", name, kv.Key, kv.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VarzHandler is an HTTP handler to write expvar values into the
|
||||
// prometheus export format:
|
||||
//
|
||||
@@ -361,74 +500,23 @@ func Error(code int, msg string, err error) HTTPError {
|
||||
// This will evolve over time, or perhaps be replaced.
|
||||
func VarzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
||||
|
||||
var dump func(prefix string, kv expvar.KeyValue)
|
||||
dump = func(prefix string, kv expvar.KeyValue) {
|
||||
name := prefix + kv.Key
|
||||
|
||||
var typ string
|
||||
switch {
|
||||
case strings.HasPrefix(kv.Key, "gauge_"):
|
||||
typ = "gauge"
|
||||
name = prefix + strings.TrimPrefix(kv.Key, "gauge_")
|
||||
|
||||
case strings.HasPrefix(kv.Key, "counter_"):
|
||||
typ = "counter"
|
||||
name = prefix + strings.TrimPrefix(kv.Key, "counter_")
|
||||
}
|
||||
|
||||
switch v := kv.Value.(type) {
|
||||
case *expvar.Int:
|
||||
if typ == "" {
|
||||
typ = "counter"
|
||||
}
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, v.Value())
|
||||
return
|
||||
case *metrics.Set:
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
dump(name+"_", kv)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if typ == "" {
|
||||
var funcRet string
|
||||
if f, ok := kv.Value.(expvar.Func); ok {
|
||||
v := f()
|
||||
if ms, ok := v.(runtime.MemStats); ok && name == "memstats" {
|
||||
writeMemstats(w, &ms)
|
||||
return
|
||||
}
|
||||
funcRet = fmt.Sprintf(" returning %T", v)
|
||||
}
|
||||
fmt.Fprintf(w, "# skipping expvar %q (Go type %T%s) with undeclared Prometheus type\n", name, kv.Value, funcRet)
|
||||
return
|
||||
}
|
||||
|
||||
switch v := kv.Value.(type) {
|
||||
case expvar.Func:
|
||||
val := v()
|
||||
switch val.(type) {
|
||||
case float64, int64, int:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, val)
|
||||
default:
|
||||
fmt.Fprintf(w, "# skipping expvar func %q returning unknown type %T\n", name, val)
|
||||
}
|
||||
|
||||
case *metrics.LabelMap:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
|
||||
// IntMap uses expvar.Map on the inside, which presorts
|
||||
// keys. The output ordering is deterministic.
|
||||
v.Do(func(kv expvar.KeyValue) {
|
||||
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, v.Label, kv.Key, kv.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
dump("", kv)
|
||||
expvarDo(func(kv expvar.KeyValue) {
|
||||
writePromExpVar(w, "", kv)
|
||||
})
|
||||
}
|
||||
|
||||
// PrometheusMetricsReflectRooter is an optional interface that expvar.Var implementations
|
||||
// can implement to indicate that they should be walked recursively with reflect to find
|
||||
// sets of fields to export.
|
||||
type PrometheusMetricsReflectRooter interface {
|
||||
expvar.Var
|
||||
|
||||
// PrometheusMetricsReflectRoot returns the struct or struct pointer to walk.
|
||||
PrometheusMetricsReflectRoot() interface{}
|
||||
}
|
||||
|
||||
var expvarDo = expvar.Do // pulled out for tests
|
||||
|
||||
func writeMemstats(w io.Writer, ms *runtime.MemStats) {
|
||||
out := func(name, typ string, v uint64, help string) {
|
||||
if help != "" {
|
||||
@@ -445,3 +533,42 @@ func writeMemstats(w io.Writer, ms *runtime.MemStats) {
|
||||
c("frees", ms.Frees, "cumulative count of heap objects freed")
|
||||
c("num_gc", uint64(ms.NumGC), "number of completed GC cycles")
|
||||
}
|
||||
|
||||
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) {
|
||||
t := rv.Type()
|
||||
for i, n := 0, t.NumField(); i < n; i++ {
|
||||
sf := t.Field(i)
|
||||
name := sf.Name
|
||||
if v := sf.Tag.Get("json"); v != "" {
|
||||
if i := strings.Index(v, ","); i != -1 {
|
||||
v = v[:i]
|
||||
}
|
||||
if v == "-" {
|
||||
// Skip it, regardless of its metrictype.
|
||||
continue
|
||||
}
|
||||
if v != "" {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
metricType := sf.Tag.Get("metrictype")
|
||||
if metricType != "" || sf.Type.Kind() == reflect.Struct {
|
||||
f(name, metricType, rv.Field(i))
|
||||
} else if sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Struct {
|
||||
fv := rv.Field(i)
|
||||
if !fv.IsNil() {
|
||||
f(name, metricType, fv.Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type expVarPromStructRoot struct{ v interface{} }
|
||||
|
||||
func (r expVarPromStructRoot) PrometheusMetricsReflectRoot() interface{} { return r.v }
|
||||
func (r expVarPromStructRoot) String() string { panic("unused") }
|
||||
|
||||
var (
|
||||
_ PrometheusMetricsReflectRooter = expVarPromStructRoot{}
|
||||
_ expvar.Var = expVarPromStructRoot{}
|
||||
)
|
||||
|
||||
@@ -8,13 +8,16 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"expvar"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -300,3 +303,234 @@ func BenchmarkLog(b *testing.B) {
|
||||
h.ServeHTTP(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarzHandler(t *testing.T) {
|
||||
t.Run("globals_log", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
VarzHandler(rec, httptest.NewRequest("GET", "/", nil))
|
||||
t.Logf("Got: %s", rec.Body.Bytes())
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
k string // key name
|
||||
v expvar.Var
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"int",
|
||||
"foo",
|
||||
new(expvar.Int),
|
||||
"# TYPE foo counter\nfoo 0\n",
|
||||
},
|
||||
{
|
||||
"int_with_type_counter",
|
||||
"counter_foo",
|
||||
new(expvar.Int),
|
||||
"# TYPE foo counter\nfoo 0\n",
|
||||
},
|
||||
{
|
||||
"int_with_type_gauge",
|
||||
"gauge_foo",
|
||||
new(expvar.Int),
|
||||
"# TYPE foo gauge\nfoo 0\n",
|
||||
},
|
||||
{
|
||||
"metrics_set",
|
||||
"s",
|
||||
&metrics.Set{
|
||||
Map: *(func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
})(),
|
||||
},
|
||||
"# TYPE s_bar counter\ns_bar 2\n# TYPE s_foo counter\ns_foo 1\n",
|
||||
},
|
||||
{
|
||||
"metrics_set_TODO_gauge_type",
|
||||
"gauge_s", // TODO(bradfitz): arguably a bug; should pass down type
|
||||
&metrics.Set{
|
||||
Map: *(func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
})(),
|
||||
},
|
||||
"# TYPE s_bar counter\ns_bar 2\n# TYPE s_foo counter\ns_foo 1\n",
|
||||
},
|
||||
{
|
||||
"expvar_map_untyped",
|
||||
"api_status_code",
|
||||
func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("2xx", 100)
|
||||
m.Add("5xx", 2)
|
||||
return m
|
||||
}(),
|
||||
"api_status_code_2xx 100\napi_status_code_5xx 2\n",
|
||||
},
|
||||
{
|
||||
"func_float64",
|
||||
"counter_x",
|
||||
expvar.Func(func() interface{} { return float64(1.2) }),
|
||||
"# TYPE x counter\nx 1.2\n",
|
||||
},
|
||||
{
|
||||
"func_float64_gauge",
|
||||
"gauge_x",
|
||||
expvar.Func(func() interface{} { return float64(1.2) }),
|
||||
"# TYPE x gauge\nx 1.2\n",
|
||||
},
|
||||
{
|
||||
"func_float64_untyped",
|
||||
"x",
|
||||
expvar.Func(func() interface{} { return float64(1.2) }),
|
||||
"x 1.2\n",
|
||||
},
|
||||
{
|
||||
"metrics_label_map",
|
||||
"counter_m",
|
||||
&metrics.LabelMap{
|
||||
Label: "label",
|
||||
Map: *(func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
})(),
|
||||
},
|
||||
"# TYPE m counter\nm{label=\"bar\"} 2\nm{label=\"foo\"} 1\n",
|
||||
},
|
||||
{
|
||||
"metrics_label_map_untyped",
|
||||
"control_save_config",
|
||||
(func() *metrics.LabelMap {
|
||||
m := &metrics.LabelMap{Label: "reason"}
|
||||
m.Add("new", 1)
|
||||
m.Add("updated", 1)
|
||||
m.Add("fun", 1)
|
||||
return m
|
||||
})(),
|
||||
"control_save_config{reason=\"fun\"} 1\ncontrol_save_config{reason=\"new\"} 1\ncontrol_save_config{reason=\"updated\"} 1\n",
|
||||
},
|
||||
{
|
||||
"expvar_label_map",
|
||||
"counter_labelmap_keyname_m",
|
||||
func() *expvar.Map {
|
||||
m := new(expvar.Map)
|
||||
m.Init()
|
||||
m.Add("foo", 1)
|
||||
m.Add("bar", 2)
|
||||
return m
|
||||
}(),
|
||||
"# TYPE m counter\nm{keyname=\"bar\"} 2\nm{keyname=\"foo\"} 1\n",
|
||||
},
|
||||
{
|
||||
"struct_reflect",
|
||||
"foo",
|
||||
someExpVarWithJSONAndPromTypes(),
|
||||
strings.TrimSpace(`
|
||||
# TYPE foo_nestvalue_foo gauge
|
||||
foo_nestvalue_foo 1
|
||||
# TYPE foo_nestvalue_bar counter
|
||||
foo_nestvalue_bar 2
|
||||
# TYPE foo_nestptr_foo gauge
|
||||
foo_nestptr_foo 10
|
||||
# TYPE foo_nestptr_bar counter
|
||||
foo_nestptr_bar 20
|
||||
# TYPE foo_curX gauge
|
||||
foo_curX 3
|
||||
# TYPE foo_totalY counter
|
||||
foo_totalY 4
|
||||
# TYPE foo_curTemp gauge
|
||||
foo_curTemp 20.6
|
||||
# TYPE foo_AnInt8 counter
|
||||
foo_AnInt8 127
|
||||
# TYPE foo_AUint16 counter
|
||||
foo_AUint16 65535
|
||||
`) + "\n",
|
||||
},
|
||||
{
|
||||
"struct_reflect_nil_root",
|
||||
"foo",
|
||||
expvarAdapter{(*SomeStats)(nil)},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"func_returning_int",
|
||||
"num_goroutines",
|
||||
expvar.Func(func() interface{} { return 123 }),
|
||||
"num_goroutines 123\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() { expvarDo = expvar.Do }()
|
||||
expvarDo = func(f func(expvar.KeyValue)) {
|
||||
f(expvar.KeyValue{Key: tt.k, Value: tt.v})
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
VarzHandler(rec, httptest.NewRequest("GET", "/", nil))
|
||||
if got := rec.Body.Bytes(); string(got) != tt.want {
|
||||
t.Errorf("mismatch\n got: %q\n%s\nwant: %q\n%s\n", got, got, tt.want, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type SomeNested struct {
|
||||
FooG int64 `json:"foo" metrictype:"gauge"`
|
||||
BarC int64 `json:"bar" metrictype:"counter"`
|
||||
Omit int `json:"-" metrictype:"counter"`
|
||||
}
|
||||
|
||||
type SomeStats struct {
|
||||
Nested SomeNested `json:"nestvalue"`
|
||||
NestedPtr *SomeNested `json:"nestptr"`
|
||||
NestedNilPtr *SomeNested `json:"nestnilptr"`
|
||||
CurX int `json:"curX" metrictype:"gauge"`
|
||||
NoMetricType int `json:"noMetric" metrictype:""`
|
||||
TotalY int64 `json:"totalY,omitempty" metrictype:"counter"`
|
||||
CurTemp float64 `json:"curTemp" metrictype:"gauge"`
|
||||
AnInt8 int8 `metrictype:"counter"`
|
||||
AUint16 uint16 `metrictype:"counter"`
|
||||
}
|
||||
|
||||
// someExpVarWithJSONAndPromTypes returns an expvar.Var that
|
||||
// implements PrometheusMetricsReflectRooter for TestVarzHandler.
|
||||
func someExpVarWithJSONAndPromTypes() expvar.Var {
|
||||
st := &SomeStats{
|
||||
Nested: SomeNested{
|
||||
FooG: 1,
|
||||
BarC: 2,
|
||||
Omit: 3,
|
||||
},
|
||||
NestedPtr: &SomeNested{
|
||||
FooG: 10,
|
||||
BarC: 20,
|
||||
},
|
||||
CurX: 3,
|
||||
TotalY: 4,
|
||||
CurTemp: 20.6,
|
||||
AnInt8: 127,
|
||||
AUint16: 65535,
|
||||
}
|
||||
return expvarAdapter{st}
|
||||
}
|
||||
|
||||
type expvarAdapter struct {
|
||||
st *SomeStats
|
||||
}
|
||||
|
||||
func (expvarAdapter) String() string { return "{}" } // expvar JSON; unused in test
|
||||
|
||||
func (a expvarAdapter) PrometheusMetricsReflectRoot() interface{} {
|
||||
return a.st
|
||||
}
|
||||
|
||||
@@ -87,6 +87,12 @@ func (nm *NetworkMap) Concise() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) VeryConcise() string {
|
||||
buf := new(strings.Builder)
|
||||
nm.printConciseHeader(buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// printConciseHeader prints a concise header line representing nm to buf.
|
||||
//
|
||||
// If this function is changed to access different fields of nm, keep
|
||||
@@ -132,7 +138,7 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
|
||||
return (a.Debug == nil && b.Debug == nil) || reflect.DeepEqual(a.Debug, b.Debug)
|
||||
}
|
||||
|
||||
// printPeerConcise appends to buf a line repsenting the peer p.
|
||||
// printPeerConcise appends to buf a line representing the peer p.
|
||||
//
|
||||
// If this function is changed to access different fields of p, keep
|
||||
// in nodeConciseEqual in sync.
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -332,15 +334,32 @@ func TestArrayAllocs(t *testing.T) {
|
||||
if version.IsRace() {
|
||||
t.Skip("skipping test under race detector")
|
||||
}
|
||||
|
||||
// In theory, there should be no allocations. However, escape analysis on
|
||||
// certain architectures fails to detect that certain cases do not escape.
|
||||
// This discrepency currently affects sha256.digest.Sum.
|
||||
// Measure the number of allocations in sha256 to ensure that Hash does
|
||||
// not allocate on top of its usage of sha256.
|
||||
// See https://golang.org/issue/48055.
|
||||
var b []byte
|
||||
h := sha256.New()
|
||||
want := int(testing.AllocsPerRun(1000, func() {
|
||||
b = h.Sum(b[:0])
|
||||
}))
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64":
|
||||
want = 0 // ensure no allocations on popular architectures
|
||||
}
|
||||
|
||||
type T struct {
|
||||
X [32]byte
|
||||
}
|
||||
x := &T{X: [32]byte{1: 1, 2: 2, 3: 3, 4: 4}}
|
||||
n := int(testing.AllocsPerRun(1000, func() {
|
||||
got := int(testing.AllocsPerRun(1000, func() {
|
||||
sink = Hash(x)
|
||||
}))
|
||||
if n > 0 {
|
||||
t.Errorf("allocs = %v; want 0", n)
|
||||
if got > want {
|
||||
t.Errorf("allocs = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (go1.16 && !ios) || (!go1.16 && !darwin) || (!go1.16 && !arm64)
|
||||
// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64
|
||||
//go:build !ios
|
||||
// +build !ios
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"rsc.io/goversion/version"
|
||||
)
|
||||
|
||||
// CmdName returns either the base name of the current binary
|
||||
@@ -30,13 +32,13 @@ func CmdName() string {
|
||||
fallbackName := filepath.Base(strings.TrimSuffix(strings.ToLower(e), ".exe"))
|
||||
|
||||
var ret string
|
||||
v, err := version.ReadExe(e)
|
||||
info, err := findModuleInfo(e)
|
||||
if err != nil {
|
||||
return fallbackName
|
||||
}
|
||||
// v is like:
|
||||
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
|
||||
for _, line := range strings.Split(v.ModuleInfo, "\n") {
|
||||
for _, line := range strings.Split(info, "\n") {
|
||||
if strings.HasPrefix(line, "path\t") {
|
||||
goPkg := strings.TrimPrefix(line, "path\t") // like "tailscale.com/cmd/tailscale"
|
||||
ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
|
||||
@@ -48,3 +50,84 @@ func CmdName() string {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// findModuleInfo returns the Go module info from the executable file.
|
||||
func findModuleInfo(file string) (s string, err error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
// Scan through f until we find infoStart.
|
||||
buf := make([]byte, 65536)
|
||||
start, err := findOffset(f, buf, infoStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
start += int64(len(infoStart))
|
||||
// Seek to the end of infoStart and scan for infoEnd.
|
||||
_, err = f.Seek(start, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
end, err := findOffset(f, buf, infoEnd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
length := end - start
|
||||
// As of Aug 2021, tailscaled's mod info was about 2k.
|
||||
if length > int64(len(buf)) {
|
||||
return "", errors.New("mod info too large")
|
||||
}
|
||||
// We have located modinfo. Read it into buf.
|
||||
buf = buf[:length]
|
||||
_, err = f.Seek(start, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = io.ReadFull(f, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// findOffset finds the absolute offset of needle in f,
|
||||
// starting at f's current read position,
|
||||
// using temporary buffer buf.
|
||||
func findOffset(f *os.File, buf, needle []byte) (int64, error) {
|
||||
for {
|
||||
// Fill buf and look within it.
|
||||
n, err := f.Read(buf)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
i := bytes.Index(buf[:n], needle)
|
||||
if i < 0 {
|
||||
// Not found. Rewind a little bit in case we happened to end halfway through needle.
|
||||
rewind, err := f.Seek(int64(-len(needle)), io.SeekCurrent)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
// If we're at EOF and rewound exactly len(needle) bytes, return io.EOF.
|
||||
_, err = f.ReadAt(buf[:1], rewind+int64(len(needle)))
|
||||
if err == io.EOF {
|
||||
return -1, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Found! Figure out exactly where.
|
||||
cur, err := f.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return cur - int64(n) + int64(i), nil
|
||||
}
|
||||
}
|
||||
|
||||
// These constants are taken from rsc.io/goversion.
|
||||
|
||||
var (
|
||||
infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6")
|
||||
infoEnd, _ = hex.DecodeString("f932433186182072008242104116d8f2")
|
||||
)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (go1.16 && ios) || (!go1.16 && darwin && arm64)
|
||||
// +build go1.16,ios !go1.16,darwin,arm64
|
||||
//go:build ios
|
||||
// +build ios
|
||||
|
||||
package version
|
||||
|
||||
|
||||
29
version/modinfo_test.go
Normal file
29
version/modinfo_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2021 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 version
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindModuleInfo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
name := filepath.Join(dir, "tailscaled-version-test")
|
||||
out, err := exec.Command("go", "build", "-o", name, "tailscale.com/cmd/tailscaled").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build tailscaled: %v\n%s", err, out)
|
||||
}
|
||||
modinfo, err := findModuleInfo(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := "path\ttailscale.com/cmd/tailscaled\nmod\ttailscale.com"
|
||||
if !strings.HasPrefix(modinfo, prefix) {
|
||||
t.Errorf("unexpected modinfo contents %q", modinfo)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ package version
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
var Long = "date.20210727"
|
||||
var Long = "date.20210823"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
|
||||
53
wgengine/magicsock/debugknobs.go
Normal file
53
wgengine/magicsock/debugknobs.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
//go:build !ios
|
||||
// +build !ios
|
||||
|
||||
package magicsock
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Various debugging and experimental tweakables, set by environment
|
||||
// variable.
|
||||
var (
|
||||
// logPacketDests prints the known addresses for a peer every time
|
||||
// they change, in the legacy (non-discovery) endpoint code only.
|
||||
logPacketDests, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_PACKET_DESTS"))
|
||||
// debugDisco prints verbose logs of active discovery events as
|
||||
// they happen.
|
||||
debugDisco, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DISCO"))
|
||||
// debugOmitLocalAddresses removes all local interface addresses
|
||||
// from magicsock's discovered local endpoints. Used in some tests.
|
||||
debugOmitLocalAddresses, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_OMIT_LOCAL_ADDRS"))
|
||||
// debugUseDerpRoute temporarily (2020-03-22) controls whether DERP
|
||||
// reverse routing is enabled (Issue 150). It will become always true
|
||||
// later.
|
||||
debugUseDerpRouteEnv = os.Getenv("TS_DEBUG_ENABLE_DERP_ROUTE")
|
||||
debugUseDerpRoute, _ = strconv.ParseBool(debugUseDerpRouteEnv)
|
||||
// logDerpVerbose logs all received DERP packets, including their
|
||||
// full payload.
|
||||
logDerpVerbose, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DERP"))
|
||||
// debugReSTUNStopOnIdle unconditionally enables the "shut down
|
||||
// STUN if magicsock is idle" behavior that normally only triggers
|
||||
// on mobile devices, lowers the shutdown interval, and logs more
|
||||
// verbosely about idle measurements.
|
||||
debugReSTUNStopOnIdle, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_RESTUN_STOP_ON_IDLE"))
|
||||
// debugAlwaysDERP disables the use of UDP, forcing all peer communication over DERP.
|
||||
debugAlwaysDERP, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ALWAYS_USE_DERP"))
|
||||
)
|
||||
|
||||
// inTest reports whether the running program is a test that set the
|
||||
// IN_TS_TEST environment variable.
|
||||
//
|
||||
// Unlike the other debug tweakables above, this one needs to be
|
||||
// checked every time at runtime, because tests set this after program
|
||||
// startup.
|
||||
func inTest() bool {
|
||||
inTest, _ := strconv.ParseBool(os.Getenv("IN_TS_TEST"))
|
||||
return inTest
|
||||
}
|
||||
20
wgengine/magicsock/debugknobs_ios.go
Normal file
20
wgengine/magicsock/debugknobs_ios.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2021 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 magicsock
|
||||
|
||||
// All knobs are disabled on iOS.
|
||||
// Further, they're const, so the toolchain can produce smaller binaries.
|
||||
const (
|
||||
logPacketDests = false
|
||||
debugDisco = false
|
||||
debugOmitLocalAddresses = false
|
||||
debugUseDerpRouteEnv = ""
|
||||
debugUseDerpRoute = false
|
||||
logDerpVerbose = false
|
||||
debugReSTUNStopOnIdle = false
|
||||
debugAlwaysDERP = false
|
||||
)
|
||||
|
||||
func inTest() bool { return false }
|
||||
@@ -59,35 +59,6 @@ import (
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
// Various debugging and experimental tweakables, set by environment
|
||||
// variable.
|
||||
var (
|
||||
// logPacketDests prints the known addresses for a peer every time
|
||||
// they change, in the legacy (non-discovery) endpoint code only.
|
||||
logPacketDests, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_PACKET_DESTS"))
|
||||
// debugDisco prints verbose logs of active discovery events as
|
||||
// they happen.
|
||||
debugDisco, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DISCO"))
|
||||
// debugOmitLocalAddresses removes all local interface addresses
|
||||
// from magicsock's discovered local endpoints. Used in some tests.
|
||||
debugOmitLocalAddresses, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_OMIT_LOCAL_ADDRS"))
|
||||
// debugUseDerpRoute temporarily (2020-03-22) controls whether DERP
|
||||
// reverse routing is enabled (Issue 150). It will become always true
|
||||
// later.
|
||||
debugUseDerpRouteEnv = os.Getenv("TS_DEBUG_ENABLE_DERP_ROUTE")
|
||||
debugUseDerpRoute, _ = strconv.ParseBool(debugUseDerpRouteEnv)
|
||||
// logDerpVerbose logs all received DERP packets, including their
|
||||
// full payload.
|
||||
logDerpVerbose, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DERP"))
|
||||
// debugReSTUNStopOnIdle unconditionally enables the "shut down
|
||||
// STUN if magicsock is idle" behavior that normally only triggers
|
||||
// on mobile devices, lowers the shutdown interval, and logs more
|
||||
// verbosely about idle measurements.
|
||||
debugReSTUNStopOnIdle, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_RESTUN_STOP_ON_IDLE"))
|
||||
// debugAlwaysDERP disables the use of UDP, forcing all peer communication over DERP.
|
||||
debugAlwaysDERP, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ALWAYS_USE_DERP"))
|
||||
)
|
||||
|
||||
// useDerpRoute reports whether magicsock should enable the DERP
|
||||
// return path optimization (Issue 150).
|
||||
func useDerpRoute() bool {
|
||||
@@ -101,17 +72,6 @@ func useDerpRoute() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// inTest reports whether the running program is a test that set the
|
||||
// IN_TS_TEST environment variable.
|
||||
//
|
||||
// Unlike the other debug tweakables above, this one needs to be
|
||||
// checked every time at runtime, because tests set this after program
|
||||
// startup.
|
||||
func inTest() bool {
|
||||
inTest, _ := strconv.ParseBool(os.Getenv("IN_TS_TEST"))
|
||||
return inTest
|
||||
}
|
||||
|
||||
// A Conn routes UDP packets and actively manages a list of its endpoints.
|
||||
// It implements wireguard/conn.Bind.
|
||||
type Conn struct {
|
||||
@@ -893,11 +853,17 @@ func (c *Conn) populateCLIPingResponseLocked(res *ipnstate.PingResult, latency t
|
||||
}
|
||||
regionID := int(ep.Port())
|
||||
res.DERPRegionID = regionID
|
||||
if c.derpMap != nil {
|
||||
if dr, ok := c.derpMap.Regions[regionID]; ok {
|
||||
res.DERPRegionCode = dr.RegionCode
|
||||
}
|
||||
res.DERPRegionCode = c.derpRegionCodeLocked(regionID)
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeLocked(regionID int) string {
|
||||
if c.derpMap == nil {
|
||||
return ""
|
||||
}
|
||||
if dr, ok := c.derpMap.Regions[regionID]; ok {
|
||||
return dr.RegionCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DiscoPublicKey returns the discovery public key.
|
||||
@@ -2800,13 +2766,14 @@ func (c *Conn) ParseEndpoint(endpointStr string) (conn.Endpoint, error) {
|
||||
}
|
||||
pk := key.Public(endpoints.PublicKey)
|
||||
discoKey := endpoints.DiscoKey
|
||||
c.logf("magicsock: ParseEndpoint: key=%s: disco=%s ipps=%s", pk.ShortString(), discoKey.ShortString(), derpStr(endpoints.IPPorts.String()))
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if discoKey.IsZero() {
|
||||
c.logf("magicsock: ParseEndpoint: key=%s: disco=%s legacy=%s", pk.ShortString(), discoKey.ShortString(), derpStr(endpoints.IPPorts.String()))
|
||||
return c.createLegacyEndpointLocked(pk, endpoints.IPPorts, endpointStr)
|
||||
}
|
||||
|
||||
de := &discoEndpoint{
|
||||
c: c,
|
||||
publicKey: tailcfg.NodeKey(pk), // peer public key (for WireGuard + DERP)
|
||||
@@ -2817,7 +2784,35 @@ func (c *Conn) ParseEndpoint(endpointStr string) (conn.Endpoint, error) {
|
||||
endpointState: map[netaddr.IPPort]*endpointState{},
|
||||
}
|
||||
de.initFakeUDPAddr()
|
||||
de.updateFromNode(c.nodeOfDisco[de.discoKey])
|
||||
n := c.nodeOfDisco[de.discoKey]
|
||||
de.updateFromNode(n)
|
||||
c.logf("magicsock: ParseEndpoint: key=%s: disco=%s; %v", pk.ShortString(), discoKey.ShortString(), logger.ArgWriter(func(w *bufio.Writer) {
|
||||
if n == nil {
|
||||
w.WriteString("nil node")
|
||||
return
|
||||
}
|
||||
const derpPrefix = "127.3.3.40:"
|
||||
if strings.HasPrefix(n.DERP, derpPrefix) {
|
||||
ipp, _ := netaddr.ParseIPPort(n.DERP)
|
||||
regionID := int(ipp.Port())
|
||||
code := c.derpRegionCodeLocked(regionID)
|
||||
if code != "" {
|
||||
code = "(" + code + ")"
|
||||
}
|
||||
fmt.Fprintf(w, "derp=%v%s ", regionID, code)
|
||||
}
|
||||
|
||||
for _, a := range n.AllowedIPs {
|
||||
if a.IsSingleIP() {
|
||||
fmt.Fprintf(w, "aip=%v ", a.IP())
|
||||
} else {
|
||||
fmt.Fprintf(w, "aip=%v ", a)
|
||||
}
|
||||
}
|
||||
for _, ep := range n.Endpoints {
|
||||
fmt.Fprintf(w, "ep=%v ", ep)
|
||||
}
|
||||
}))
|
||||
c.endpointOfDisco[de.discoKey] = de
|
||||
return de, nil
|
||||
}
|
||||
|
||||
@@ -468,19 +468,37 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
return filter.DropSilently
|
||||
}
|
||||
|
||||
func netaddrIPFromNetstackIP(s tcpip.Address) netaddr.IP {
|
||||
switch len(s) {
|
||||
case 4:
|
||||
return netaddr.IPv4(s[0], s[1], s[2], s[3])
|
||||
case 16:
|
||||
var a [16]byte
|
||||
copy(a[:], s)
|
||||
return netaddr.IPFrom16(a)
|
||||
}
|
||||
return netaddr.IP{}
|
||||
}
|
||||
|
||||
func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
reqDetails := r.ID()
|
||||
if debugNetstack {
|
||||
ns.logf("[v2] TCP ForwarderRequest: %s", stringifyTEI(reqDetails))
|
||||
}
|
||||
dialAddr := reqDetails.LocalAddress
|
||||
dialNetAddr, _ := netaddr.FromStdIP(net.IP(dialAddr))
|
||||
isTailscaleIP := tsaddr.IsTailscaleIP(dialNetAddr)
|
||||
clientRemoteIP := netaddrIPFromNetstackIP(reqDetails.RemoteAddress)
|
||||
if !clientRemoteIP.IsValid() {
|
||||
ns.logf("invalid RemoteAddress in TCP ForwarderRequest: %s", stringifyTEI(reqDetails))
|
||||
r.Complete(true)
|
||||
return
|
||||
}
|
||||
|
||||
dialIP := netaddrIPFromNetstackIP(reqDetails.LocalAddress)
|
||||
isTailscaleIP := tsaddr.IsTailscaleIP(dialIP)
|
||||
defer func() {
|
||||
if !isTailscaleIP {
|
||||
// if this is a subnet IP, we added this in before the TCP handshake
|
||||
// so netstack is happy TCP-handshaking as a subnet IP
|
||||
ns.removeSubnetAddress(dialNetAddr)
|
||||
ns.removeSubnetAddress(dialIP)
|
||||
}
|
||||
}()
|
||||
var wq waiter.Queue
|
||||
@@ -490,21 +508,31 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
return
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
// Asynchronously start the TCP handshake. Note that the
|
||||
// gonet.TCPConn methods c.RemoteAddr() and c.LocalAddr() will
|
||||
// return nil until the handshake actually completes. But we
|
||||
// have the remote address in reqDetails instead, so we don't
|
||||
// use RemoteAddr. The byte copies in both directions in
|
||||
// forwardTCP will block until the TCP handshake is complete.
|
||||
c := gonet.NewTCPConn(&wq, ep)
|
||||
|
||||
if ns.ForwardTCPIn != nil {
|
||||
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
||||
return
|
||||
}
|
||||
if isTailscaleIP {
|
||||
dialAddr = tcpip.Address(net.ParseIP("127.0.0.1")).To4()
|
||||
dialIP = netaddr.IPv4(127, 0, 0, 1)
|
||||
}
|
||||
ns.forwardTCP(c, &wq, dialAddr, reqDetails.LocalPort)
|
||||
dialAddr := netaddr.IPPortFrom(dialIP, uint16(reqDetails.LocalPort))
|
||||
ns.forwardTCP(c, clientRemoteIP, &wq, dialAddr)
|
||||
}
|
||||
|
||||
func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, dialAddr tcpip.Address, dialPort uint16) {
|
||||
func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netaddr.IP, wq *waiter.Queue, dialAddr netaddr.IPPort) {
|
||||
defer client.Close()
|
||||
dialAddrStr := net.JoinHostPort(dialAddr.String(), strconv.Itoa(int(dialPort)))
|
||||
dialAddrStr := dialAddr.String()
|
||||
ns.logf("[v2] netstack: forwarding incoming connection to %s", dialAddrStr)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
waitEntry, notifyCh := waiter.NewChannelEntry(nil)
|
||||
@@ -530,7 +558,6 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, dialAddr tcp
|
||||
defer server.Close()
|
||||
backendLocalAddr := server.LocalAddr().(*net.TCPAddr)
|
||||
backendLocalIPPort, _ := netaddr.FromStdAddr(backendLocalAddr.IP, backendLocalAddr.Port, backendLocalAddr.Zone)
|
||||
clientRemoteIP, _ := netaddr.FromStdIP(client.RemoteAddr().(*net.TCPAddr).IP)
|
||||
ns.e.RegisterIPPortIdentity(backendLocalIPPort, clientRemoteIP)
|
||||
defer ns.e.UnregisterIPPortIdentity(backendLocalIPPort)
|
||||
connClosed := make(chan error, 2)
|
||||
|
||||
@@ -155,7 +155,11 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, linkMon *monitor.Mo
|
||||
}
|
||||
}
|
||||
|
||||
return newUserspaceRouterAdvanced(logf, tunname, linkMon, ipt4, ipt6, osCommandRunner{}, supportsV6, supportsV6NAT)
|
||||
cmd := osCommandRunner{
|
||||
ambientCapNetAdmin: distro.Get() == distro.Synology,
|
||||
}
|
||||
|
||||
return newUserspaceRouterAdvanced(logf, tunname, linkMon, ipt4, ipt6, cmd, supportsV6, supportsV6NAT)
|
||||
}
|
||||
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, linkMon *monitor.Mon, netfilter4, netfilter6 netfilterRunner, cmd commandRunner, supportsV6, supportsV6NAT bool) (Router, error) {
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// commandRunner abstracts helpers to run OS commands. It exists
|
||||
@@ -23,7 +26,16 @@ type commandRunner interface {
|
||||
output(...string) ([]byte, error)
|
||||
}
|
||||
|
||||
type osCommandRunner struct{}
|
||||
type osCommandRunner struct {
|
||||
// ambientCapNetAdmin determines whether commands are executed with
|
||||
// CAP_NET_ADMIN.
|
||||
// CAP_NET_ADMIN is required when running as non-root and executing cmds
|
||||
// like `ip rule`. Even if our process has the capability, we need to
|
||||
// explicitly grant it to the new process.
|
||||
// We specifically need this for Synology DSM7 where tailscaled no longer
|
||||
// runs as root.
|
||||
ambientCapNetAdmin bool
|
||||
}
|
||||
|
||||
// errCode extracts and returns the process exit code from err, or
|
||||
// zero if err is nil.
|
||||
@@ -55,7 +67,13 @@ func (o osCommandRunner) output(args ...string) ([]byte, error) {
|
||||
return nil, errors.New("cmd: no argv[0]")
|
||||
}
|
||||
|
||||
out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
if o.ambientCapNetAdmin {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
AmbientCaps: []uintptr{unix.CAP_NET_ADMIN},
|
||||
}
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running %q failed: %w\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ type userspaceEngine struct {
|
||||
wgLogger *wglog.Logger //a wireguard-go logging wrapper
|
||||
reqCh chan struct{}
|
||||
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
|
||||
magicConnStarted chan struct{} // chan is closed after magicConn.Start
|
||||
timeNow func() mono.Time
|
||||
tundev *tstun.Wrapper
|
||||
wgdev *device.Device
|
||||
@@ -108,6 +109,7 @@ type userspaceEngine struct {
|
||||
|
||||
wgLock sync.Mutex // serializes all wgdev operations; see lock order comment below
|
||||
lastCfgFull wgcfg.Config
|
||||
lastNMinPeers int
|
||||
lastRouterSig deephash.Sum // of router.Config
|
||||
lastEngineSigFull deephash.Sum // of full wireguard config
|
||||
lastEngineSigTrim deephash.Sum // of trimmed wireguard config
|
||||
@@ -248,13 +250,14 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
closePool.add(tsTUNDev)
|
||||
|
||||
e := &userspaceEngine{
|
||||
timeNow: mono.Now,
|
||||
logf: logf,
|
||||
reqCh: make(chan struct{}, 1),
|
||||
waitCh: make(chan struct{}),
|
||||
tundev: tsTUNDev,
|
||||
router: conf.Router,
|
||||
confListenPort: conf.ListenPort,
|
||||
timeNow: mono.Now,
|
||||
logf: logf,
|
||||
reqCh: make(chan struct{}, 1),
|
||||
waitCh: make(chan struct{}),
|
||||
tundev: tsTUNDev,
|
||||
router: conf.Router,
|
||||
confListenPort: conf.ListenPort,
|
||||
magicConnStarted: make(chan struct{}),
|
||||
}
|
||||
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(nil))
|
||||
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(nil))
|
||||
@@ -370,7 +373,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
|
||||
// It's a little pointless to apply no-op settings here (they
|
||||
// should already be empty?), but it at least exercises the
|
||||
// router implementation early on the machine.
|
||||
// router implementation early on.
|
||||
e.logf("Clearing router settings...")
|
||||
if err := e.router.Set(nil); err != nil {
|
||||
return nil, err
|
||||
@@ -379,6 +382,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
e.linkMon.Start()
|
||||
e.logf("Starting magicsock...")
|
||||
e.magicConn.Start()
|
||||
close(e.magicConnStarted)
|
||||
|
||||
go e.pollResolver()
|
||||
|
||||
@@ -606,7 +610,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
// based on the full config. Prune off all the peers
|
||||
// and only add the active ones back.
|
||||
min := full
|
||||
min.Peers = nil
|
||||
min.Peers = make([]wgcfg.Peer, 0, e.lastNMinPeers)
|
||||
|
||||
// We'll only keep a peer around if it's been active in
|
||||
// the past 5 minutes. That's more than WireGuard's key
|
||||
@@ -650,6 +654,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
trimmedDisco[dk] = true
|
||||
}
|
||||
}
|
||||
e.lastNMinPeers = len(min.Peers)
|
||||
|
||||
if !deephash.Update(&e.lastEngineSigTrim, &min, trimmedDisco, trackDisco, trackIPs) {
|
||||
// No changes
|
||||
@@ -921,8 +926,8 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
pp := make(map[wgkey.Key]*ipnstate.PeerStatusLite)
|
||||
p := &ipnstate.PeerStatusLite{}
|
||||
pp := make(map[wgkey.Key]ipnstate.PeerStatusLite)
|
||||
var p ipnstate.PeerStatusLite
|
||||
|
||||
var hst1, hst2, n int64
|
||||
|
||||
@@ -954,11 +959,10 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("IpcGetOperation: invalid key in line %q", line)
|
||||
}
|
||||
p = &ipnstate.PeerStatusLite{}
|
||||
pp[wgkey.Key(pk)] = p
|
||||
|
||||
key := tailcfg.NodeKey(pk)
|
||||
p.NodeKey = key
|
||||
if !p.NodeKey.IsZero() {
|
||||
pp[wgkey.Key(p.NodeKey)] = p
|
||||
}
|
||||
p = ipnstate.PeerStatusLite{NodeKey: tailcfg.NodeKey(pk)}
|
||||
case "rx_bytes":
|
||||
n, err = mem.ParseInt(v, 10, 64)
|
||||
p.RxBytes = n
|
||||
@@ -986,6 +990,9 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
} // else leave at time.IsZero()
|
||||
}
|
||||
}
|
||||
if !p.NodeKey.IsZero() {
|
||||
pp[wgkey.Key(p.NodeKey)] = p
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
return nil, fmt.Errorf("IpcGetOperation: %v", err)
|
||||
}
|
||||
@@ -993,10 +1000,19 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
var peers []ipnstate.PeerStatusLite
|
||||
// Do two passes, one to calculate size and the other to populate.
|
||||
// This code is sensitive to allocations.
|
||||
npeers := 0
|
||||
for _, pk := range e.peerSequence {
|
||||
if _, ok := pp[pk]; ok { // ignore idle ones not in wireguard-go's config
|
||||
npeers++
|
||||
}
|
||||
}
|
||||
|
||||
peers := make([]ipnstate.PeerStatusLite, 0, npeers)
|
||||
for _, pk := range e.peerSequence {
|
||||
if p, ok := pp[pk]; ok { // ignore idle ones not in wireguard-go's config
|
||||
peers = append(peers, *p)
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,6 +1095,10 @@ func (e *userspaceEngine) LinkChange(_ bool) {
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) {
|
||||
// Issue 2733: wait for e.magicConn to be started; there's two tiny
|
||||
// windows at startup where this callback can be run before Start
|
||||
<-e.magicConnStarted
|
||||
|
||||
up := cur.AnyInterfaceUp()
|
||||
if !up {
|
||||
e.logf("LinkChange: all links down; pausing: %v", cur)
|
||||
|
||||
158
words/scales.txt
Normal file
158
words/scales.txt
Normal file
@@ -0,0 +1,158 @@
|
||||
lizard
|
||||
crocodile
|
||||
snake
|
||||
dragon
|
||||
catfish
|
||||
bass
|
||||
salmon
|
||||
tuna
|
||||
hammerhead
|
||||
eel
|
||||
carp
|
||||
trout
|
||||
mahi
|
||||
snapper
|
||||
bluegill
|
||||
sole
|
||||
cod
|
||||
triceratops
|
||||
edmontosaurus
|
||||
saurolophus
|
||||
liberty
|
||||
justice
|
||||
pangolin
|
||||
turtle
|
||||
tortoise
|
||||
alligator
|
||||
butterfly
|
||||
iguana
|
||||
pineapplefish
|
||||
anaconda
|
||||
puffin
|
||||
cardassian
|
||||
cloud
|
||||
nominal
|
||||
ordinal
|
||||
interval
|
||||
ratio
|
||||
vernier
|
||||
lime
|
||||
scala
|
||||
boa
|
||||
cobra
|
||||
richter
|
||||
kelvin
|
||||
celsius
|
||||
decibel
|
||||
beaufort
|
||||
mohs
|
||||
pauling
|
||||
python
|
||||
mamba
|
||||
alkaline
|
||||
climb
|
||||
moth
|
||||
eagle
|
||||
woodpecker
|
||||
morpho
|
||||
tautara
|
||||
anoles
|
||||
theopods
|
||||
owl
|
||||
frog
|
||||
lionfish
|
||||
morray
|
||||
clownfish
|
||||
ostrich
|
||||
stork
|
||||
egret
|
||||
map
|
||||
grue
|
||||
tiyanki
|
||||
broadnose
|
||||
basking
|
||||
goblin
|
||||
porbeagle
|
||||
chuckwalla
|
||||
tawny
|
||||
bramble
|
||||
kitefin
|
||||
agamas
|
||||
komodo
|
||||
bull
|
||||
monitor
|
||||
chameleon
|
||||
tegus
|
||||
gecko
|
||||
micro
|
||||
gray
|
||||
allosaurus
|
||||
glyptodon
|
||||
basilisk
|
||||
cordylus
|
||||
tegu
|
||||
sailfin
|
||||
mountain
|
||||
dinosaur
|
||||
goanna
|
||||
herring
|
||||
minnow
|
||||
perch
|
||||
coho
|
||||
lake
|
||||
arctic
|
||||
pumpkinseed
|
||||
salamander
|
||||
mudpuppy
|
||||
gopher
|
||||
chickadee
|
||||
toad
|
||||
shark
|
||||
roach
|
||||
tyrannosaurus
|
||||
velociraptor
|
||||
|
||||
# Musical scales
|
||||
acoustic
|
||||
alpha
|
||||
altered
|
||||
augmented
|
||||
bebop
|
||||
beta
|
||||
blues
|
||||
bushi
|
||||
byzantine
|
||||
chromatic
|
||||
delta
|
||||
diatonic
|
||||
diminished
|
||||
dominant
|
||||
dorian
|
||||
enigmatic
|
||||
freygish
|
||||
gamma
|
||||
harmonic
|
||||
heptatonic
|
||||
hexatonic
|
||||
hirajoshi
|
||||
in
|
||||
insen
|
||||
istrian
|
||||
iwato
|
||||
jazz
|
||||
locrian
|
||||
major
|
||||
minor
|
||||
mixolydian
|
||||
musical
|
||||
octatonic
|
||||
pentatonic
|
||||
phrygian
|
||||
pierce
|
||||
prometheus
|
||||
pythagorean
|
||||
symmetric
|
||||
tet
|
||||
tone
|
||||
tritone
|
||||
yo
|
||||
211
words/tails.txt
Normal file
211
words/tails.txt
Normal file
@@ -0,0 +1,211 @@
|
||||
orca
|
||||
shark
|
||||
capybara
|
||||
fox
|
||||
turtle
|
||||
fish
|
||||
dolphin
|
||||
armadillo
|
||||
hedgehog
|
||||
dhole
|
||||
dog
|
||||
cat
|
||||
jaguar
|
||||
cheetah
|
||||
leopard
|
||||
cougar
|
||||
lion
|
||||
tapir
|
||||
anteater
|
||||
monkey
|
||||
snake
|
||||
scorpion
|
||||
jerboa
|
||||
opossum
|
||||
stingray
|
||||
colobus
|
||||
euplectes
|
||||
jay
|
||||
finch
|
||||
hawk
|
||||
beaver
|
||||
mouse
|
||||
moose
|
||||
alligator
|
||||
salamander
|
||||
tadpole
|
||||
astrapia
|
||||
pug
|
||||
greyhound
|
||||
foxhound
|
||||
bushbaby
|
||||
aardvark
|
||||
aardwolf
|
||||
pangolin
|
||||
porcupine
|
||||
genet
|
||||
springhare
|
||||
kangaroo
|
||||
koala
|
||||
ostrich
|
||||
dingo
|
||||
platypus
|
||||
camel
|
||||
horse
|
||||
echidna
|
||||
wombat
|
||||
crocodile
|
||||
whale
|
||||
narwhal
|
||||
humpback
|
||||
marmoset
|
||||
tucuxi
|
||||
beluga
|
||||
porpoise
|
||||
sparrow
|
||||
pigeon
|
||||
owl
|
||||
hummingbird
|
||||
robin
|
||||
starling
|
||||
manakin
|
||||
warbler
|
||||
penguin
|
||||
snowfinch
|
||||
broadbill
|
||||
thrush
|
||||
bishop
|
||||
swallow
|
||||
bittern
|
||||
caracara
|
||||
manx
|
||||
possum
|
||||
lemur
|
||||
deer
|
||||
peacock
|
||||
ratfish
|
||||
vulture
|
||||
rat
|
||||
takaya
|
||||
skunk
|
||||
tuxedo
|
||||
turkey
|
||||
elephant
|
||||
civet
|
||||
ainu
|
||||
husky
|
||||
akita
|
||||
alpaca
|
||||
spaniel
|
||||
terrier
|
||||
hare
|
||||
auroch
|
||||
axolotl
|
||||
bandicoot
|
||||
beagle
|
||||
beago
|
||||
collie
|
||||
tiger
|
||||
liger
|
||||
rhino
|
||||
bobcat
|
||||
corgi
|
||||
dachshund
|
||||
giraffe
|
||||
bonobo
|
||||
cetacean
|
||||
bear
|
||||
hyena
|
||||
burmese
|
||||
caracal
|
||||
goat
|
||||
chameleon
|
||||
chihuahua
|
||||
chimp
|
||||
cow
|
||||
cuscus
|
||||
pinscher
|
||||
dunker
|
||||
mau
|
||||
ermine
|
||||
feist
|
||||
spitz
|
||||
squirrel
|
||||
gerbil
|
||||
hampster
|
||||
panda
|
||||
gibbon
|
||||
flyingfox
|
||||
jackal
|
||||
macaque
|
||||
kinkajou
|
||||
lynx
|
||||
manatee
|
||||
mole
|
||||
ocelot
|
||||
otter
|
||||
panther
|
||||
pig
|
||||
prairiedog
|
||||
puma
|
||||
hippo
|
||||
quokka
|
||||
quoll
|
||||
bunny
|
||||
raccoon
|
||||
tamarin
|
||||
reindeer
|
||||
hyrax
|
||||
saiga
|
||||
sable
|
||||
serval
|
||||
spanador
|
||||
springbok
|
||||
tuatara
|
||||
dropbear
|
||||
vaquita
|
||||
wallaby
|
||||
walrus
|
||||
weasel
|
||||
wolf
|
||||
zebra
|
||||
zonkey
|
||||
donkey
|
||||
mule
|
||||
zebu
|
||||
cuttlefish
|
||||
unicorn
|
||||
meerkat
|
||||
jackalope
|
||||
heron
|
||||
fawn
|
||||
warthog
|
||||
drake
|
||||
badger
|
||||
zapus
|
||||
yak
|
||||
werewolf
|
||||
tahr
|
||||
fossa
|
||||
xerus
|
||||
centaur
|
||||
raptor
|
||||
long
|
||||
sheep
|
||||
quetzal
|
||||
wildebeest
|
||||
motmot
|
||||
coati
|
||||
drongo
|
||||
boston
|
||||
myth
|
||||
saga
|
||||
fable
|
||||
folk
|
||||
fairy
|
||||
hound
|
||||
risk
|
||||
coin
|
||||
tyrannosaurus
|
||||
velociraptor
|
||||
siren
|
||||
59
words/words.go
Normal file
59
words/words.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2021 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 words contains accessors for some nice words.
|
||||
package words
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed tails.txt
|
||||
var tailsTxt []byte
|
||||
|
||||
//go:embed scales.txt
|
||||
var scalesTxt []byte
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
tails, scales []string
|
||||
)
|
||||
|
||||
// Tails returns words about tails.
|
||||
func Tails() []string {
|
||||
once.Do(initWords)
|
||||
return tails
|
||||
}
|
||||
|
||||
// Scales returns words about scales.
|
||||
func Scales() []string {
|
||||
once.Do(initWords)
|
||||
return scales
|
||||
}
|
||||
|
||||
func initWords() {
|
||||
tails = parseWords(tailsTxt)
|
||||
scales = parseWords(scalesTxt)
|
||||
}
|
||||
|
||||
func parseWords(txt []byte) []string {
|
||||
n := bytes.Count(txt, []byte{'\n'})
|
||||
ret := make([]string, 0, n)
|
||||
for len(txt) > 0 {
|
||||
word := txt
|
||||
i := bytes.IndexByte(txt, '\n')
|
||||
if i != -1 {
|
||||
word, txt = word[:i], txt[i+1:]
|
||||
} else {
|
||||
txt = nil
|
||||
}
|
||||
if word := strings.TrimSpace(string(word)); word != "" && word[0] != '#' {
|
||||
ret = append(ret, word)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
39
words/words_test.go
Normal file
39
words/words_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2021 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 words
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWords(t *testing.T) {
|
||||
test := func(t *testing.T, words []string) {
|
||||
t.Helper()
|
||||
if len(words) == 0 {
|
||||
t.Error("no words")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, w := range words {
|
||||
if seen[w] {
|
||||
t.Errorf("dup word %q", w)
|
||||
}
|
||||
seen[w] = true
|
||||
if w == "" || strings.IndexFunc(w, nonASCIILower) != -1 {
|
||||
t.Errorf("malformed word %q", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("tails", func(t *testing.T) { test(t, Tails()) })
|
||||
t.Run("scales", func(t *testing.T) { test(t, Scales()) })
|
||||
t.Logf("%v tails * %v scales = %v beautiful combinations", len(Tails()), len(Scales()), len(Tails())*len(Scales()))
|
||||
}
|
||||
|
||||
func nonASCIILower(r rune) bool {
|
||||
if 'a' <= r && r <= 'z' {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user