Compare commits
82 Commits
dsnet/admi
...
danderson/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0a1375eb | ||
|
|
a35c3ba221 | ||
|
|
83906abc5e | ||
|
|
ae9b3f38d6 | ||
|
|
baf8854f9a | ||
|
|
3606e68721 | ||
|
|
4aab083cae | ||
|
|
b49d9bc74d | ||
|
|
1925fb584e | ||
|
|
88bd796622 | ||
|
|
ac0353e982 | ||
|
|
780e65a613 | ||
|
|
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 |
20
.github/workflows/staticcheck.yml
vendored
20
.github/workflows/staticcheck.yml
vendored
@@ -31,16 +31,28 @@ jobs:
|
||||
run: "staticcheck -version"
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: "386"
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
@@ -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.15.0
|
||||
|
||||
47
api.md
47
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)
|
||||
@@ -473,7 +474,7 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
|
||||
###### Query Parameters
|
||||
`type` - can be 'user' or 'ipport'
|
||||
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
|
||||
The provided ACL is queried with this paramater to determine which rules match.
|
||||
The provided ACL is queried with this parameter to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
<div class="text-right truncate leading-4">
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<h4 class="truncate leading-normal">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -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
|
||||
@@ -46,7 +45,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
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+
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
ifconfig bool // print network state once and exit
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
@@ -45,6 +46,7 @@ var debugModeFunc = debugMode // so it can be addressable
|
||||
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
@@ -59,8 +61,11 @@ func debugMode(args []string) error {
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.ifconfig {
|
||||
return runMonitor(ctx, false)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
return runMonitor(ctx, true)
|
||||
}
|
||||
if debugArgs.portmap {
|
||||
return debugPortmap(ctx)
|
||||
@@ -71,7 +76,7 @@ func debugMode(args []string) error {
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
func runMonitor(ctx context.Context, loop bool) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
@@ -88,8 +93,13 @@ func runMonitor(ctx context.Context) error {
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
if loop {
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
}
|
||||
dump(mon.InterfaceState())
|
||||
if !loop {
|
||||
return nil
|
||||
}
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
|
||||
@@ -87,7 +87,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 +127,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/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 +157,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 +165,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 +177,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 +216,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 +241,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 +254,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 +282,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+
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
@@ -33,7 +34,6 @@ import (
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -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
|
||||
@@ -350,24 +381,33 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
}
|
||||
useNetstack = name == "userspace-networking"
|
||||
if !useNetstack {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
return nil, false, err
|
||||
// dev, devName, err := tstun.New(logf, name)
|
||||
// if err != nil {
|
||||
// tstun.Diagnose(logf, name)
|
||||
// return nil, false, err
|
||||
// }
|
||||
// conf.Tun = dev
|
||||
// if strings.HasPrefix(name, "tap:") {
|
||||
// conf.IsTAP = true
|
||||
// e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
// return e, false, err
|
||||
// }
|
||||
|
||||
// HACK
|
||||
exec.Command("ip", "link", "del", "tailscale0").Run()
|
||||
if err := exec.Command("ip", "link", "add", "tailscale0", "type", "wireguard").Run(); err != nil {
|
||||
return nil, false, fmt.Errorf("create device: %v", err)
|
||||
}
|
||||
conf.Tun = dev
|
||||
if strings.HasPrefix(name, "tap:") {
|
||||
conf.IsTAP = true
|
||||
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
return e, false, err
|
||||
if err := exec.Command("ip", "link", "set", "tailscale0", "up").Run(); err != nil {
|
||||
return nil, false, fmt.Errorf("create device: %v", err)
|
||||
}
|
||||
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
r, err := router.New(logf, nil, linkMon)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
//dev.Close()
|
||||
return nil, false, err
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
d, err := dns.NewOSConfigurator(logf, "tailscale0")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -377,7 +417,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
}
|
||||
}
|
||||
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
||||
e, err = wgengine.NewKernelEngine(logf, conf)
|
||||
if err != nil {
|
||||
return nil, useNetstack, err
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -865,22 +823,21 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
// a lite map update occured meanwhile. This only affects
|
||||
// a lite map update occurred meanwhile. This only affects
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ type Server struct {
|
||||
|
||||
// PacketForwarder is something that can forward packets.
|
||||
//
|
||||
// It's mostly an inteface for circular dependency reasons; the
|
||||
// It's mostly an interface for circular dependency reasons; the
|
||||
// typical implementation is derphttp.Client. The other implementation
|
||||
// is a multiForwarder, which this package creates as needed if a
|
||||
// public key gets more than one PacketForwarder registered for it.
|
||||
@@ -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,
|
||||
|
||||
4
go.mod
4
go.mod
@@ -40,16 +40,16 @@ 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
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16
|
||||
honnef.co/go/tools v0.1.4
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1
|
||||
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
|
||||
)
|
||||
|
||||
13
go.sum
13
go.sum
@@ -421,6 +421,7 @@ github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
|
||||
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
@@ -666,6 +667,7 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -733,6 +735,7 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@@ -804,14 +807,17 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
@@ -890,8 +896,11 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0 h1:qINUmOnDCCF7i14oomDDkGmlda7BSDTGfge77/aqdfk=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16 h1:S42i0kp3SFHZm1mMFTtiU3OnEQJ0GRVOVlMkBhSDTZI=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16/go.mod h1:f80rkFY2CKQklps1GHE15k/M4Tq78aofbr1iQM5MTVY=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -987,5 +996,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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
|
||||
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
|
||||
return netaddr.IP{}, dns.RCodeNotImplemented
|
||||
|
||||
// For everything except for the few types above that are explictly not implemented, return no records.
|
||||
// For everything except for the few types above that are explicitly not implemented, return no records.
|
||||
// This is what other DNS systems do: always return NOERROR
|
||||
// without any records whenever the requested record type is unknown.
|
||||
// You can try this with:
|
||||
|
||||
@@ -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,7 +49,7 @@ type entry struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache, set or updating its assoicated
|
||||
// Add adds a value to the cache, set or updating its associated
|
||||
// value.
|
||||
//
|
||||
// If MaxEntries is non-zero and the length of the cache is greater
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -362,7 +362,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
tries = 2
|
||||
} else if hadBoth {
|
||||
// For dual stack machines, make the 3rd & slower nodes alternate
|
||||
// beetween.
|
||||
// between.
|
||||
if ri%2 == 0 {
|
||||
do4, do6 = true, false
|
||||
} else {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ func socketPermissionsForOS() os.FileMode {
|
||||
// connectMacOSAppSandbox connects to the Tailscale Network Extension,
|
||||
// which is necessarily running within the macOS App Sandbox. Our
|
||||
// little dance to connect a regular user binary to the sandboxed
|
||||
// nework extension is:
|
||||
// network extension is:
|
||||
//
|
||||
// * the sandboxed IPNExtension picks a random localhost:0 TCP port
|
||||
// to listen on
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
This is a copy of the `tunnel/firewall` package of
|
||||
https://git.zx2c4.com/wireguard-windows, with some hardcoded filter
|
||||
rules adjusted to function with Tailscale, rather than
|
||||
wireguard-windows's process structure.
|
||||
|
||||
You should not use this package. It exists as a band-aid while we
|
||||
figure out how to upstream a more flexible firewall package that does
|
||||
the fancier things we need, while also supporting wireguard-windows's
|
||||
goals.
|
||||
@@ -1,191 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type wfpObjectInstaller func(uintptr) error
|
||||
|
||||
//
|
||||
// Fundamental WireGuard specific WFP objects.
|
||||
//
|
||||
type baseObjects struct {
|
||||
provider windows.GUID
|
||||
filters windows.GUID
|
||||
}
|
||||
|
||||
var wfpSession uintptr
|
||||
|
||||
func createWfpSession() (uintptr, error) {
|
||||
sessionDisplayData, err := createWtFwpmDisplayData0("WireGuard", "WireGuard dynamic session")
|
||||
if err != nil {
|
||||
return 0, wrapErr(err)
|
||||
}
|
||||
|
||||
session := wtFwpmSession0{
|
||||
displayData: *sessionDisplayData,
|
||||
flags: cFWPM_SESSION_FLAG_DYNAMIC,
|
||||
txnWaitTimeoutInMSec: windows.INFINITE,
|
||||
}
|
||||
|
||||
sessionHandle := uintptr(0)
|
||||
|
||||
err = fwpmEngineOpen0(nil, cRPC_C_AUTHN_WINNT, nil, &session, unsafe.Pointer(&sessionHandle))
|
||||
if err != nil {
|
||||
return 0, wrapErr(err)
|
||||
}
|
||||
|
||||
return sessionHandle, nil
|
||||
}
|
||||
|
||||
func registerBaseObjects(session uintptr) (*baseObjects, error) {
|
||||
bo := &baseObjects{}
|
||||
var err error
|
||||
bo.provider, err = windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
bo.filters, err = windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
|
||||
//
|
||||
// Register provider.
|
||||
//
|
||||
{
|
||||
displayData, err := createWtFwpmDisplayData0("WireGuard", "WireGuard provider")
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
provider := wtFwpmProvider0{
|
||||
providerKey: bo.provider,
|
||||
displayData: *displayData,
|
||||
}
|
||||
err = fwpmProviderAdd0(session, &provider, 0)
|
||||
if err != nil {
|
||||
// TODO: cleanup entire call chain of these if failure?
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Register filters sublayer.
|
||||
//
|
||||
{
|
||||
displayData, err := createWtFwpmDisplayData0("WireGuard filters", "Permissive and blocking filters")
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
sublayer := wtFwpmSublayer0{
|
||||
subLayerKey: bo.filters,
|
||||
displayData: *displayData,
|
||||
providerKey: &bo.provider,
|
||||
weight: ^uint16(0),
|
||||
}
|
||||
err = fwpmSubLayerAdd0(session, &sublayer, 0)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return bo, nil
|
||||
}
|
||||
|
||||
func EnableFirewall(luid uint64, doNotRestrict bool, restrictToDNSServers []net.IP) error {
|
||||
if wfpSession != 0 {
|
||||
return errors.New("The firewall has already been enabled")
|
||||
}
|
||||
|
||||
session, err := createWfpSession()
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
objectInstaller := func(session uintptr) error {
|
||||
baseObjects, err := registerBaseObjects(session)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = permitWireGuardService(session, baseObjects, 15)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
if !doNotRestrict {
|
||||
err = allowDNS(session, baseObjects)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = permitLoopback(session, baseObjects, 13)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = permitTunInterface(session, baseObjects, 12, luid)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = permitDHCPIPv4(session, baseObjects, 12)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = permitDHCPIPv6(session, baseObjects, 12)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = permitNdp(session, baseObjects, 12)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
/* TODO: actually evaluate if this does anything and if we need this. It's layer 2; our other rules are layer 3.
|
||||
* In other words, if somebody complains, try enabling it. For now, keep it off.
|
||||
err = permitHyperV(session, baseObjects, 12)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
*/
|
||||
|
||||
err = blockAll(session, baseObjects, 0)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = runTransaction(session, objectInstaller)
|
||||
if err != nil {
|
||||
fwpmEngineClose0(session)
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
wfpSession = session
|
||||
return nil
|
||||
}
|
||||
|
||||
func DisableFirewall() {
|
||||
if wfpSession != 0 {
|
||||
fwpmEngineClose0(wfpSession)
|
||||
wfpSession = 0
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func runTransaction(session uintptr, operation wfpObjectInstaller) error {
|
||||
err := fwpmTransactionBegin0(session, 0)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = operation(session)
|
||||
if err != nil {
|
||||
fwpmTransactionAbort0(session)
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
err = fwpmTransactionCommit0(session)
|
||||
if err != nil {
|
||||
fwpmTransactionAbort0(session)
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWtFwpmDisplayData0(name, description string) (*wtFwpmDisplayData0, error) {
|
||||
namePtr, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
|
||||
descriptionPtr, err := windows.UTF16PtrFromString(description)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
|
||||
return &wtFwpmDisplayData0{
|
||||
name: namePtr,
|
||||
description: descriptionPtr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func filterWeight(weight uint8) wtFwpValue0 {
|
||||
return wtFwpValue0{
|
||||
_type: cFWP_UINT8,
|
||||
value: uintptr(weight),
|
||||
}
|
||||
}
|
||||
|
||||
func wrapErr(err error) error {
|
||||
if _, ok := err.(syscall.Errno); !ok {
|
||||
return err
|
||||
}
|
||||
_, file, line, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
return fmt.Errorf("Firewall error at unknown location: %w", err)
|
||||
}
|
||||
return fmt.Errorf("Firewall error at %s:%d: %w", file, line, err)
|
||||
}
|
||||
|
||||
func getCurrentProcessSecurityDescriptor() (*windows.SECURITY_DESCRIPTOR, error) {
|
||||
var processToken windows.Token
|
||||
err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &processToken)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
defer processToken.Close()
|
||||
gs, err := processToken.GetTokenGroups()
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
var sid *windows.SID
|
||||
for _, g := range gs.AllGroups() {
|
||||
if g.Attributes != windows.SE_GROUP_ENABLED|windows.SE_GROUP_ENABLED_BY_DEFAULT|windows.SE_GROUP_OWNER {
|
||||
continue
|
||||
}
|
||||
// We could be checking != 6, but hopefully Microsoft will update
|
||||
// RtlCreateServiceSid to use SHA2, which will then likely bump
|
||||
// this up. So instead just roll with a minimum.
|
||||
if !g.Sid.IsValid() || g.Sid.IdentifierAuthority() != windows.SECURITY_NT_AUTHORITY || g.Sid.SubAuthorityCount() < 6 || g.Sid.SubAuthority(0) != 80 {
|
||||
continue
|
||||
}
|
||||
sid = g.Sid
|
||||
break
|
||||
}
|
||||
if sid == nil {
|
||||
return nil, wrapErr(windows.ERROR_NO_SUCH_GROUP)
|
||||
}
|
||||
|
||||
access := []windows.EXPLICIT_ACCESS{{
|
||||
AccessPermissions: cFWP_ACTRL_MATCH_FILTER,
|
||||
AccessMode: windows.GRANT_ACCESS,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_GROUP,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||
},
|
||||
}}
|
||||
dacl, err := windows.ACLFromEntries(access, nil)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
sd, err := windows.NewSecurityDescriptor()
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
err = sd.SetDACL(dacl, true, false)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
sd, err = sd.ToSelfRelative()
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
return sd, nil
|
||||
}
|
||||
|
||||
func getCurrentProcessAppID() (*wtFwpByteBlob, error) {
|
||||
currentFile, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
|
||||
curFilePtr, err := windows.UTF16PtrFromString(currentFile)
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
|
||||
var appID *wtFwpByteBlob
|
||||
err = fwpmGetAppIdFromFileName0(curFilePtr, unsafe.Pointer(&appID))
|
||||
if err != nil {
|
||||
return nil, wrapErr(err)
|
||||
}
|
||||
return appID, nil
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineopen0
|
||||
//sys fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmEngineOpen0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineclose0
|
||||
//sys fwpmEngineClose0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmEngineClose0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmsublayeradd0
|
||||
//sys fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmSubLayerAdd0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmgetappidfromfilename0
|
||||
//sys fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmGetAppIdFromFileName0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfreememory0
|
||||
//sys fwpmFreeMemory0(p unsafe.Pointer) = fwpuclnt.FwpmFreeMemory0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfilteradd0
|
||||
//sys fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) [failretval!=0] = fwpuclnt.FwpmFilterAdd0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/Fwpmu/nf-fwpmu-fwpmtransactionbegin0
|
||||
//sys fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionBegin0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactioncommit0
|
||||
//sys fwpmTransactionCommit0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionCommit0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactionabort0
|
||||
//sys fwpmTransactionAbort0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionAbort0
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmprovideradd0
|
||||
//sys fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmProviderAdd0
|
||||
@@ -1,413 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
const (
|
||||
anysizeArray = 1 // ANYSIZE_ARRAY defined in winnt.h
|
||||
|
||||
wtFwpBitmapArray64_Size = 8
|
||||
|
||||
wtFwpByteArray16_Size = 16
|
||||
|
||||
wtFwpByteArray6_Size = 6
|
||||
|
||||
wtFwpmAction0_Size = 20
|
||||
wtFwpmAction0_filterType_Offset = 4
|
||||
|
||||
wtFwpV4AddrAndMask_Size = 8
|
||||
wtFwpV4AddrAndMask_mask_Offset = 4
|
||||
|
||||
wtFwpV6AddrAndMask_Size = 17
|
||||
wtFwpV6AddrAndMask_prefixLength_Offset = 16
|
||||
)
|
||||
|
||||
type wtFwpActionFlag uint32
|
||||
|
||||
const (
|
||||
cFWP_ACTION_FLAG_TERMINATING wtFwpActionFlag = 0x00001000
|
||||
cFWP_ACTION_FLAG_NON_TERMINATING wtFwpActionFlag = 0x00002000
|
||||
cFWP_ACTION_FLAG_CALLOUT wtFwpActionFlag = 0x00004000
|
||||
)
|
||||
|
||||
// FWP_ACTION_TYPE defined in fwptypes.h
|
||||
type wtFwpActionType uint32
|
||||
|
||||
const (
|
||||
cFWP_ACTION_BLOCK wtFwpActionType = wtFwpActionType(0x00000001 | cFWP_ACTION_FLAG_TERMINATING)
|
||||
cFWP_ACTION_PERMIT wtFwpActionType = wtFwpActionType(0x00000002 | cFWP_ACTION_FLAG_TERMINATING)
|
||||
cFWP_ACTION_CALLOUT_TERMINATING wtFwpActionType = wtFwpActionType(0x00000003 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_TERMINATING)
|
||||
cFWP_ACTION_CALLOUT_INSPECTION wtFwpActionType = wtFwpActionType(0x00000004 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_NON_TERMINATING)
|
||||
cFWP_ACTION_CALLOUT_UNKNOWN wtFwpActionType = wtFwpActionType(0x00000005 | cFWP_ACTION_FLAG_CALLOUT)
|
||||
cFWP_ACTION_CONTINUE wtFwpActionType = wtFwpActionType(0x00000006 | cFWP_ACTION_FLAG_NON_TERMINATING)
|
||||
cFWP_ACTION_NONE wtFwpActionType = 0x00000007
|
||||
cFWP_ACTION_NONE_NO_MATCH wtFwpActionType = 0x00000008
|
||||
cFWP_ACTION_BITMAP_INDEX_SET wtFwpActionType = 0x00000009
|
||||
)
|
||||
|
||||
// FWP_BYTE_BLOB defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_blob_)
|
||||
type wtFwpByteBlob struct {
|
||||
size uint32
|
||||
data *uint8
|
||||
}
|
||||
|
||||
// FWP_MATCH_TYPE defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_match_type_)
|
||||
type wtFwpMatchType uint32
|
||||
|
||||
const (
|
||||
cFWP_MATCH_EQUAL wtFwpMatchType = 0
|
||||
cFWP_MATCH_GREATER wtFwpMatchType = cFWP_MATCH_EQUAL + 1
|
||||
cFWP_MATCH_LESS wtFwpMatchType = cFWP_MATCH_GREATER + 1
|
||||
cFWP_MATCH_GREATER_OR_EQUAL wtFwpMatchType = cFWP_MATCH_LESS + 1
|
||||
cFWP_MATCH_LESS_OR_EQUAL wtFwpMatchType = cFWP_MATCH_GREATER_OR_EQUAL + 1
|
||||
cFWP_MATCH_RANGE wtFwpMatchType = cFWP_MATCH_LESS_OR_EQUAL + 1
|
||||
cFWP_MATCH_FLAGS_ALL_SET wtFwpMatchType = cFWP_MATCH_RANGE + 1
|
||||
cFWP_MATCH_FLAGS_ANY_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ALL_SET + 1
|
||||
cFWP_MATCH_FLAGS_NONE_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ANY_SET + 1
|
||||
cFWP_MATCH_EQUAL_CASE_INSENSITIVE wtFwpMatchType = cFWP_MATCH_FLAGS_NONE_SET + 1
|
||||
cFWP_MATCH_NOT_EQUAL wtFwpMatchType = cFWP_MATCH_EQUAL_CASE_INSENSITIVE + 1
|
||||
cFWP_MATCH_PREFIX wtFwpMatchType = cFWP_MATCH_NOT_EQUAL + 1
|
||||
cFWP_MATCH_NOT_PREFIX wtFwpMatchType = cFWP_MATCH_PREFIX + 1
|
||||
cFWP_MATCH_TYPE_MAX wtFwpMatchType = cFWP_MATCH_NOT_PREFIX + 1
|
||||
)
|
||||
|
||||
// FWPM_ACTION0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_action0_)
|
||||
type wtFwpmAction0 struct {
|
||||
_type wtFwpActionType
|
||||
filterType windows.GUID // Windows type: GUID
|
||||
}
|
||||
|
||||
// Defined in fwpmu.h. 4cd62a49-59c3-4969-b7f3-bda5d32890a4
|
||||
var cFWPM_CONDITION_IP_LOCAL_INTERFACE = windows.GUID{
|
||||
Data1: 0x4cd62a49,
|
||||
Data2: 0x59c3,
|
||||
Data3: 0x4969,
|
||||
Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4},
|
||||
}
|
||||
|
||||
// Defined in fwpmu.h. b235ae9a-1d64-49b8-a44c-5ff3d9095045
|
||||
var cFWPM_CONDITION_IP_REMOTE_ADDRESS = windows.GUID{
|
||||
Data1: 0xb235ae9a,
|
||||
Data2: 0x1d64,
|
||||
Data3: 0x49b8,
|
||||
Data4: [8]byte{0xa4, 0x4c, 0x5f, 0xf3, 0xd9, 0x09, 0x50, 0x45},
|
||||
}
|
||||
|
||||
// Defined in fwpmu.h. 3971ef2b-623e-4f9a-8cb1-6e79b806b9a7
|
||||
var cFWPM_CONDITION_IP_PROTOCOL = windows.GUID{
|
||||
Data1: 0x3971ef2b,
|
||||
Data2: 0x623e,
|
||||
Data3: 0x4f9a,
|
||||
Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7},
|
||||
}
|
||||
|
||||
// Defined in fwpmu.h. 0c1ba1af-5765-453f-af22-a8f791ac775b
|
||||
var cFWPM_CONDITION_IP_LOCAL_PORT = windows.GUID{
|
||||
Data1: 0x0c1ba1af,
|
||||
Data2: 0x5765,
|
||||
Data3: 0x453f,
|
||||
Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b},
|
||||
}
|
||||
|
||||
// Defined in fwpmu.h. c35a604d-d22b-4e1a-91b4-68f674ee674b
|
||||
var cFWPM_CONDITION_IP_REMOTE_PORT = windows.GUID{
|
||||
Data1: 0xc35a604d,
|
||||
Data2: 0xd22b,
|
||||
Data3: 0x4e1a,
|
||||
Data4: [8]byte{0x91, 0xb4, 0x68, 0xf6, 0x74, 0xee, 0x67, 0x4b},
|
||||
}
|
||||
|
||||
// Defined in fwpmu.h. d78e1e87-8644-4ea5-9437-d809ecefc971
|
||||
var cFWPM_CONDITION_ALE_APP_ID = windows.GUID{
|
||||
Data1: 0xd78e1e87,
|
||||
Data2: 0x8644,
|
||||
Data3: 0x4ea5,
|
||||
Data4: [8]byte{0x94, 0x37, 0xd8, 0x09, 0xec, 0xef, 0xc9, 0x71},
|
||||
}
|
||||
|
||||
// af043a0a-b34d-4f86-979c-c90371af6e66
|
||||
var cFWPM_CONDITION_ALE_USER_ID = windows.GUID{
|
||||
Data1: 0xaf043a0a,
|
||||
Data2: 0xb34d,
|
||||
Data3: 0x4f86,
|
||||
Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66},
|
||||
}
|
||||
|
||||
// d9ee00de-c1ef-4617-bfe3-ffd8f5a08957
|
||||
var cFWPM_CONDITION_IP_LOCAL_ADDRESS = windows.GUID{
|
||||
Data1: 0xd9ee00de,
|
||||
Data2: 0xc1ef,
|
||||
Data3: 0x4617,
|
||||
Data4: [8]byte{0xbf, 0xe3, 0xff, 0xd8, 0xf5, 0xa0, 0x89, 0x57},
|
||||
}
|
||||
|
||||
var cFWPM_CONDITION_ICMP_TYPE = cFWPM_CONDITION_IP_LOCAL_PORT
|
||||
var cFWPM_CONDITION_ICMP_CODE = cFWPM_CONDITION_IP_REMOTE_PORT
|
||||
|
||||
// 7bc43cbf-37ba-45f1-b74a-82ff518eeb10
|
||||
var cFWPM_CONDITION_L2_FLAGS = windows.GUID{
|
||||
Data1: 0x7bc43cbf,
|
||||
Data2: 0x37ba,
|
||||
Data3: 0x45f1,
|
||||
Data4: [8]byte{0xb7, 0x4a, 0x82, 0xff, 0x51, 0x8e, 0xeb, 0x10},
|
||||
}
|
||||
|
||||
type wtFwpmL2Flags uint32
|
||||
|
||||
const cFWP_CONDITION_L2_IS_VM2VM wtFwpmL2Flags = 0x00000010
|
||||
|
||||
var cFWPM_CONDITION_FLAGS = windows.GUID{
|
||||
Data1: 0x632ce23b,
|
||||
Data2: 0x5167,
|
||||
Data3: 0x435c,
|
||||
Data4: [8]byte{0x86, 0xd7, 0xe9, 0x03, 0x68, 0x4a, 0xa8, 0x0c},
|
||||
}
|
||||
|
||||
type wtFwpmFlags uint32
|
||||
|
||||
const cFWP_CONDITION_FLAG_IS_LOOPBACK wtFwpmFlags = 0x00000001
|
||||
|
||||
// Defined in fwpmtypes.h
|
||||
type wtFwpmFilterFlags uint32
|
||||
|
||||
const (
|
||||
cFWPM_FILTER_FLAG_NONE wtFwpmFilterFlags = 0x00000000
|
||||
cFWPM_FILTER_FLAG_PERSISTENT wtFwpmFilterFlags = 0x00000001
|
||||
cFWPM_FILTER_FLAG_BOOTTIME wtFwpmFilterFlags = 0x00000002
|
||||
cFWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000004
|
||||
cFWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT wtFwpmFilterFlags = 0x00000008
|
||||
cFWPM_FILTER_FLAG_PERMIT_IF_CALLOUT_UNREGISTERED wtFwpmFilterFlags = 0x00000010
|
||||
cFWPM_FILTER_FLAG_DISABLED wtFwpmFilterFlags = 0x00000020
|
||||
cFWPM_FILTER_FLAG_INDEXED wtFwpmFilterFlags = 0x00000040
|
||||
cFWPM_FILTER_FLAG_HAS_SECURITY_REALM_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000080
|
||||
cFWPM_FILTER_FLAG_SYSTEMOS_ONLY wtFwpmFilterFlags = 0x00000100
|
||||
cFWPM_FILTER_FLAG_GAMEOS_ONLY wtFwpmFilterFlags = 0x00000200
|
||||
cFWPM_FILTER_FLAG_SILENT_MODE wtFwpmFilterFlags = 0x00000400
|
||||
cFWPM_FILTER_FLAG_IPSEC_NO_ACQUIRE_INITIATE wtFwpmFilterFlags = 0x00000800
|
||||
)
|
||||
|
||||
// FWPM_LAYER_ALE_AUTH_CONNECT_V4 (c38d57d1-05a7-4c33-904f-7fbceee60e82) defined in fwpmu.h
|
||||
var cFWPM_LAYER_ALE_AUTH_CONNECT_V4 = windows.GUID{
|
||||
Data1: 0xc38d57d1,
|
||||
Data2: 0x05a7,
|
||||
Data3: 0x4c33,
|
||||
Data4: [8]byte{0x90, 0x4f, 0x7f, 0xbc, 0xee, 0xe6, 0x0e, 0x82},
|
||||
}
|
||||
|
||||
// e1cd9fe7-f4b5-4273-96c0-592e487b8650
|
||||
var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 = windows.GUID{
|
||||
Data1: 0xe1cd9fe7,
|
||||
Data2: 0xf4b5,
|
||||
Data3: 0x4273,
|
||||
Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50},
|
||||
}
|
||||
|
||||
// FWPM_LAYER_ALE_AUTH_CONNECT_V6 (4a72393b-319f-44bc-84c3-ba54dcb3b6b4) defined in fwpmu.h
|
||||
var cFWPM_LAYER_ALE_AUTH_CONNECT_V6 = windows.GUID{
|
||||
Data1: 0x4a72393b,
|
||||
Data2: 0x319f,
|
||||
Data3: 0x44bc,
|
||||
Data4: [8]byte{0x84, 0xc3, 0xba, 0x54, 0xdc, 0xb3, 0xb6, 0xb4},
|
||||
}
|
||||
|
||||
// a3b42c97-9f04-4672-b87e-cee9c483257f
|
||||
var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 = windows.GUID{
|
||||
Data1: 0xa3b42c97,
|
||||
Data2: 0x9f04,
|
||||
Data3: 0x4672,
|
||||
Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f},
|
||||
}
|
||||
|
||||
// 94c44912-9d6f-4ebf-b995-05ab8a088d1b
|
||||
var cFWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE = windows.GUID{
|
||||
Data1: 0x94c44912,
|
||||
Data2: 0x9d6f,
|
||||
Data3: 0x4ebf,
|
||||
Data4: [8]byte{0xb9, 0x95, 0x05, 0xab, 0x8a, 0x08, 0x8d, 0x1b},
|
||||
}
|
||||
|
||||
// d4220bd3-62ce-4f08-ae88-b56e8526df50
|
||||
var cFWPM_LAYER_INBOUND_MAC_FRAME_NATIVE = windows.GUID{
|
||||
Data1: 0xd4220bd3,
|
||||
Data2: 0x62ce,
|
||||
Data3: 0x4f08,
|
||||
Data4: [8]byte{0xae, 0x88, 0xb5, 0x6e, 0x85, 0x26, 0xdf, 0x50},
|
||||
}
|
||||
|
||||
// FWP_BITMAP_ARRAY64 defined in fwtypes.h
|
||||
type wtFwpBitmapArray64 struct {
|
||||
bitmapArray64 [8]uint8 // Windows type: [8]UINT8
|
||||
}
|
||||
|
||||
// FWP_BYTE_ARRAY6 defined in fwtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array6_)
|
||||
type wtFwpByteArray6 struct {
|
||||
byteArray6 [6]uint8 // Windows type: [6]UINT8
|
||||
}
|
||||
|
||||
// FWP_BYTE_ARRAY16 defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array16_)
|
||||
type wtFwpByteArray16 struct {
|
||||
byteArray16 [16]uint8 // Windows type [16]UINT8
|
||||
}
|
||||
|
||||
// FWP_CONDITION_VALUE0 defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_condition_value0).
|
||||
type wtFwpConditionValue0 wtFwpValue0
|
||||
|
||||
// FWP_DATA_TYPE defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_data_type_)
|
||||
type wtFwpDataType uint
|
||||
|
||||
const (
|
||||
cFWP_EMPTY wtFwpDataType = 0
|
||||
cFWP_UINT8 wtFwpDataType = cFWP_EMPTY + 1
|
||||
cFWP_UINT16 wtFwpDataType = cFWP_UINT8 + 1
|
||||
cFWP_UINT32 wtFwpDataType = cFWP_UINT16 + 1
|
||||
cFWP_UINT64 wtFwpDataType = cFWP_UINT32 + 1
|
||||
cFWP_INT8 wtFwpDataType = cFWP_UINT64 + 1
|
||||
cFWP_INT16 wtFwpDataType = cFWP_INT8 + 1
|
||||
cFWP_INT32 wtFwpDataType = cFWP_INT16 + 1
|
||||
cFWP_INT64 wtFwpDataType = cFWP_INT32 + 1
|
||||
cFWP_FLOAT wtFwpDataType = cFWP_INT64 + 1
|
||||
cFWP_DOUBLE wtFwpDataType = cFWP_FLOAT + 1
|
||||
cFWP_BYTE_ARRAY16_TYPE wtFwpDataType = cFWP_DOUBLE + 1
|
||||
cFWP_BYTE_BLOB_TYPE wtFwpDataType = cFWP_BYTE_ARRAY16_TYPE + 1
|
||||
cFWP_SID wtFwpDataType = cFWP_BYTE_BLOB_TYPE + 1
|
||||
cFWP_SECURITY_DESCRIPTOR_TYPE wtFwpDataType = cFWP_SID + 1
|
||||
cFWP_TOKEN_INFORMATION_TYPE wtFwpDataType = cFWP_SECURITY_DESCRIPTOR_TYPE + 1
|
||||
cFWP_TOKEN_ACCESS_INFORMATION_TYPE wtFwpDataType = cFWP_TOKEN_INFORMATION_TYPE + 1
|
||||
cFWP_UNICODE_STRING_TYPE wtFwpDataType = cFWP_TOKEN_ACCESS_INFORMATION_TYPE + 1
|
||||
cFWP_BYTE_ARRAY6_TYPE wtFwpDataType = cFWP_UNICODE_STRING_TYPE + 1
|
||||
cFWP_BITMAP_INDEX_TYPE wtFwpDataType = cFWP_BYTE_ARRAY6_TYPE + 1
|
||||
cFWP_BITMAP_ARRAY64_TYPE wtFwpDataType = cFWP_BITMAP_INDEX_TYPE + 1
|
||||
cFWP_SINGLE_DATA_TYPE_MAX wtFwpDataType = 0xff
|
||||
cFWP_V4_ADDR_MASK wtFwpDataType = cFWP_SINGLE_DATA_TYPE_MAX + 1
|
||||
cFWP_V6_ADDR_MASK wtFwpDataType = cFWP_V4_ADDR_MASK + 1
|
||||
cFWP_RANGE_TYPE wtFwpDataType = cFWP_V6_ADDR_MASK + 1
|
||||
cFWP_DATA_TYPE_MAX wtFwpDataType = cFWP_RANGE_TYPE + 1
|
||||
)
|
||||
|
||||
// FWP_V4_ADDR_AND_MASK defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v4_addr_and_mask).
|
||||
type wtFwpV4AddrAndMask struct {
|
||||
addr uint32
|
||||
mask uint32
|
||||
}
|
||||
|
||||
// FWP_V6_ADDR_AND_MASK defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v6_addr_and_mask).
|
||||
type wtFwpV6AddrAndMask struct {
|
||||
addr [16]uint8
|
||||
prefixLength uint8
|
||||
}
|
||||
|
||||
// FWP_VALUE0 defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_value0_)
|
||||
type wtFwpValue0 struct {
|
||||
_type wtFwpDataType
|
||||
value uintptr
|
||||
}
|
||||
|
||||
// FWPM_DISPLAY_DATA0 defined in fwptypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwpm_display_data0).
|
||||
type wtFwpmDisplayData0 struct {
|
||||
name *uint16 // Windows type: *wchar_t
|
||||
description *uint16 // Windows type: *wchar_t
|
||||
}
|
||||
|
||||
// FWPM_FILTER_CONDITION0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter_condition0).
|
||||
type wtFwpmFilterCondition0 struct {
|
||||
fieldKey windows.GUID // Windows type: GUID
|
||||
matchType wtFwpMatchType
|
||||
conditionValue wtFwpConditionValue0
|
||||
}
|
||||
|
||||
// FWPM_PROVIDER0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0_)
|
||||
type wtFwpProvider0 struct {
|
||||
providerKey windows.GUID // Windows type: GUID
|
||||
displayData wtFwpmDisplayData0
|
||||
flags uint32
|
||||
providerData wtFwpByteBlob
|
||||
serviceName *uint16 // Windows type: *wchar_t
|
||||
}
|
||||
|
||||
type wtFwpmSessionFlagsValue uint32
|
||||
|
||||
const (
|
||||
cFWPM_SESSION_FLAG_DYNAMIC wtFwpmSessionFlagsValue = 0x00000001 // FWPM_SESSION_FLAG_DYNAMIC defined in fwpmtypes.h
|
||||
)
|
||||
|
||||
// FWPM_SESSION0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_session0).
|
||||
type wtFwpmSession0 struct {
|
||||
sessionKey windows.GUID // Windows type: GUID
|
||||
displayData wtFwpmDisplayData0
|
||||
flags wtFwpmSessionFlagsValue // Windows type UINT32
|
||||
txnWaitTimeoutInMSec uint32
|
||||
processId uint32 // Windows type: DWORD
|
||||
sid *windows.SID
|
||||
username *uint16 // Windows type: *wchar_t
|
||||
kernelMode uint8 // Windows type: BOOL
|
||||
}
|
||||
|
||||
type wtFwpmSublayerFlags uint32
|
||||
|
||||
const (
|
||||
cFWPM_SUBLAYER_FLAG_PERSISTENT wtFwpmSublayerFlags = 0x00000001 // FWPM_SUBLAYER_FLAG_PERSISTENT defined in fwpmtypes.h
|
||||
)
|
||||
|
||||
// FWPM_SUBLAYER0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_sublayer0_)
|
||||
type wtFwpmSublayer0 struct {
|
||||
subLayerKey windows.GUID // Windows type: GUID
|
||||
displayData wtFwpmDisplayData0
|
||||
flags wtFwpmSublayerFlags
|
||||
providerKey *windows.GUID // Windows type: *GUID
|
||||
providerData wtFwpByteBlob
|
||||
weight uint16
|
||||
}
|
||||
|
||||
// Defined in rpcdce.h
|
||||
type wtRpcCAuthN uint32
|
||||
|
||||
const (
|
||||
cRPC_C_AUTHN_NONE wtRpcCAuthN = 0
|
||||
cRPC_C_AUTHN_WINNT wtRpcCAuthN = 10
|
||||
cRPC_C_AUTHN_DEFAULT wtRpcCAuthN = 0xFFFFFFFF
|
||||
)
|
||||
|
||||
// FWPM_PROVIDER0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/sv-se/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0).
|
||||
type wtFwpmProvider0 struct {
|
||||
providerKey windows.GUID
|
||||
displayData wtFwpmDisplayData0
|
||||
flags uint32
|
||||
providerData wtFwpByteBlob
|
||||
serviceName *uint16
|
||||
}
|
||||
|
||||
type wtIPProto uint32
|
||||
|
||||
const (
|
||||
cIPPROTO_ICMP wtIPProto = 1
|
||||
cIPPROTO_ICMPV6 wtIPProto = 58
|
||||
cIPPROTO_TCP wtIPProto = 6
|
||||
cIPPROTO_UDP wtIPProto = 17
|
||||
)
|
||||
|
||||
const (
|
||||
cFWP_ACTRL_MATCH_FILTER = 1
|
||||
)
|
||||
@@ -1,91 +0,0 @@
|
||||
//go:build (windows && 386) || (windows && arm)
|
||||
// +build windows,386 windows,arm
|
||||
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
const (
|
||||
wtFwpByteBlob_Size = 8
|
||||
wtFwpByteBlob_data_Offset = 4
|
||||
|
||||
wtFwpConditionValue0_Size = 8
|
||||
wtFwpConditionValue0_uint8_Offset = 4
|
||||
|
||||
wtFwpmDisplayData0_Size = 8
|
||||
wtFwpmDisplayData0_description_Offset = 4
|
||||
|
||||
wtFwpmFilter0_Size = 152
|
||||
wtFwpmFilter0_displayData_Offset = 16
|
||||
wtFwpmFilter0_flags_Offset = 24
|
||||
wtFwpmFilter0_providerKey_Offset = 28
|
||||
wtFwpmFilter0_providerData_Offset = 32
|
||||
wtFwpmFilter0_layerKey_Offset = 40
|
||||
wtFwpmFilter0_subLayerKey_Offset = 56
|
||||
wtFwpmFilter0_weight_Offset = 72
|
||||
wtFwpmFilter0_numFilterConditions_Offset = 80
|
||||
wtFwpmFilter0_filterCondition_Offset = 84
|
||||
wtFwpmFilter0_action_Offset = 88
|
||||
wtFwpmFilter0_providerContextKey_Offset = 112
|
||||
wtFwpmFilter0_reserved_Offset = 128
|
||||
wtFwpmFilter0_filterID_Offset = 136
|
||||
wtFwpmFilter0_effectiveWeight_Offset = 144
|
||||
|
||||
wtFwpmFilterCondition0_Size = 28
|
||||
wtFwpmFilterCondition0_matchType_Offset = 16
|
||||
wtFwpmFilterCondition0_conditionValue_Offset = 20
|
||||
|
||||
wtFwpmSession0_Size = 48
|
||||
wtFwpmSession0_displayData_Offset = 16
|
||||
wtFwpmSession0_flags_Offset = 24
|
||||
wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 28
|
||||
wtFwpmSession0_processId_Offset = 32
|
||||
wtFwpmSession0_sid_Offset = 36
|
||||
wtFwpmSession0_username_Offset = 40
|
||||
wtFwpmSession0_kernelMode_Offset = 44
|
||||
|
||||
wtFwpmSublayer0_Size = 44
|
||||
wtFwpmSublayer0_displayData_Offset = 16
|
||||
wtFwpmSublayer0_flags_Offset = 24
|
||||
wtFwpmSublayer0_providerKey_Offset = 28
|
||||
wtFwpmSublayer0_providerData_Offset = 32
|
||||
wtFwpmSublayer0_weight_Offset = 40
|
||||
|
||||
wtFwpProvider0_Size = 40
|
||||
wtFwpProvider0_displayData_Offset = 16
|
||||
wtFwpProvider0_flags_Offset = 24
|
||||
wtFwpProvider0_providerData_Offset = 28
|
||||
wtFwpProvider0_serviceName_Offset = 36
|
||||
|
||||
wtFwpTokenInformation_Size = 16
|
||||
|
||||
wtFwpValue0_Size = 8
|
||||
wtFwpValue0_value_Offset = 4
|
||||
)
|
||||
|
||||
// FWPM_FILTER0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0).
|
||||
type wtFwpmFilter0 struct {
|
||||
filterKey windows.GUID // Windows type: GUID
|
||||
displayData wtFwpmDisplayData0
|
||||
flags wtFwpmFilterFlags
|
||||
providerKey *windows.GUID // Windows type: *GUID
|
||||
providerData wtFwpByteBlob
|
||||
layerKey windows.GUID // Windows type: GUID
|
||||
subLayerKey windows.GUID // Windows type: GUID
|
||||
weight wtFwpValue0
|
||||
numFilterConditions uint32
|
||||
filterCondition *wtFwpmFilterCondition0
|
||||
action wtFwpmAction0
|
||||
offset1 [4]byte // Layout correction field
|
||||
providerContextKey windows.GUID // Windows type: GUID
|
||||
reserved *windows.GUID // Windows type: *GUID
|
||||
offset2 [4]byte // Layout correction field
|
||||
filterID uint64
|
||||
effectiveWeight wtFwpValue0
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
//go:build (windows && amd64) || (windows && arm64)
|
||||
// +build windows,amd64 windows,arm64
|
||||
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
const (
|
||||
wtFwpByteBlob_Size = 16
|
||||
wtFwpByteBlob_data_Offset = 8
|
||||
|
||||
wtFwpConditionValue0_Size = 16
|
||||
wtFwpConditionValue0_uint8_Offset = 8
|
||||
|
||||
wtFwpmDisplayData0_Size = 16
|
||||
wtFwpmDisplayData0_description_Offset = 8
|
||||
|
||||
wtFwpmFilter0_Size = 200
|
||||
wtFwpmFilter0_displayData_Offset = 16
|
||||
wtFwpmFilter0_flags_Offset = 32
|
||||
wtFwpmFilter0_providerKey_Offset = 40
|
||||
wtFwpmFilter0_providerData_Offset = 48
|
||||
wtFwpmFilter0_layerKey_Offset = 64
|
||||
wtFwpmFilter0_subLayerKey_Offset = 80
|
||||
wtFwpmFilter0_weight_Offset = 96
|
||||
wtFwpmFilter0_numFilterConditions_Offset = 112
|
||||
wtFwpmFilter0_filterCondition_Offset = 120
|
||||
wtFwpmFilter0_action_Offset = 128
|
||||
wtFwpmFilter0_providerContextKey_Offset = 152
|
||||
wtFwpmFilter0_reserved_Offset = 168
|
||||
wtFwpmFilter0_filterID_Offset = 176
|
||||
wtFwpmFilter0_effectiveWeight_Offset = 184
|
||||
|
||||
wtFwpmFilterCondition0_Size = 40
|
||||
wtFwpmFilterCondition0_matchType_Offset = 16
|
||||
wtFwpmFilterCondition0_conditionValue_Offset = 24
|
||||
|
||||
wtFwpmSession0_Size = 72
|
||||
wtFwpmSession0_displayData_Offset = 16
|
||||
wtFwpmSession0_flags_Offset = 32
|
||||
wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 36
|
||||
wtFwpmSession0_processId_Offset = 40
|
||||
wtFwpmSession0_sid_Offset = 48
|
||||
wtFwpmSession0_username_Offset = 56
|
||||
wtFwpmSession0_kernelMode_Offset = 64
|
||||
|
||||
wtFwpmSublayer0_Size = 72
|
||||
wtFwpmSublayer0_displayData_Offset = 16
|
||||
wtFwpmSublayer0_flags_Offset = 32
|
||||
wtFwpmSublayer0_providerKey_Offset = 40
|
||||
wtFwpmSublayer0_providerData_Offset = 48
|
||||
wtFwpmSublayer0_weight_Offset = 64
|
||||
|
||||
wtFwpProvider0_Size = 64
|
||||
wtFwpProvider0_displayData_Offset = 16
|
||||
wtFwpProvider0_flags_Offset = 32
|
||||
wtFwpProvider0_providerData_Offset = 40
|
||||
wtFwpProvider0_serviceName_Offset = 56
|
||||
|
||||
wtFwpValue0_Size = 16
|
||||
wtFwpValue0_value_Offset = 8
|
||||
)
|
||||
|
||||
// FWPM_FILTER0 defined in fwpmtypes.h
|
||||
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0).
|
||||
type wtFwpmFilter0 struct {
|
||||
filterKey windows.GUID // Windows type: GUID
|
||||
displayData wtFwpmDisplayData0
|
||||
flags wtFwpmFilterFlags // Windows type: UINT32
|
||||
providerKey *windows.GUID // Windows type: *GUID
|
||||
providerData wtFwpByteBlob
|
||||
layerKey windows.GUID // Windows type: GUID
|
||||
subLayerKey windows.GUID // Windows type: GUID
|
||||
weight wtFwpValue0
|
||||
numFilterConditions uint32
|
||||
filterCondition *wtFwpmFilterCondition0
|
||||
action wtFwpmAction0
|
||||
offset1 [4]byte // Layout correction field
|
||||
providerContextKey windows.GUID // Windows type: GUID
|
||||
reserved *windows.GUID // Windows type: *GUID
|
||||
filterID uint64
|
||||
effectiveWeight wtFwpValue0
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestWtFwpByteBlobSize(t *testing.T) {
|
||||
|
||||
const actualWtFwpByteBlobSize = unsafe.Sizeof(wtFwpByteBlob{})
|
||||
|
||||
if actualWtFwpByteBlobSize != wtFwpByteBlob_Size {
|
||||
t.Errorf("Size of FwpByteBlob is %d, although %d is expected.", actualWtFwpByteBlobSize,
|
||||
wtFwpByteBlob_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpByteBlobOffsets(t *testing.T) {
|
||||
|
||||
s := wtFwpByteBlob{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.data)) - sp
|
||||
|
||||
if offset != wtFwpByteBlob_data_Offset {
|
||||
t.Errorf("FwpByteBlob.data offset is %d although %d is expected", offset, wtFwpByteBlob_data_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmAction0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpmAction0Size = unsafe.Sizeof(wtFwpmAction0{})
|
||||
|
||||
if actualWtFwpmAction0Size != wtFwpmAction0_Size {
|
||||
t.Errorf("Size of wtFwpmAction0 is %d, although %d is expected.", actualWtFwpmAction0Size,
|
||||
wtFwpmAction0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmAction0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpmAction0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.filterType)) - sp
|
||||
|
||||
if offset != wtFwpmAction0_filterType_Offset {
|
||||
t.Errorf("wtFwpmAction0.filterType offset is %d although %d is expected", offset,
|
||||
wtFwpmAction0_filterType_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpBitmapArray64Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpBitmapArray64Size = unsafe.Sizeof(wtFwpBitmapArray64{})
|
||||
|
||||
if actualWtFwpBitmapArray64Size != wtFwpBitmapArray64_Size {
|
||||
t.Errorf("Size of wtFwpBitmapArray64 is %d, although %d is expected.", actualWtFwpBitmapArray64Size,
|
||||
wtFwpBitmapArray64_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpByteArray6Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpByteArray6Size = unsafe.Sizeof(wtFwpByteArray6{})
|
||||
|
||||
if actualWtFwpByteArray6Size != wtFwpByteArray6_Size {
|
||||
t.Errorf("Size of wtFwpByteArray6 is %d, although %d is expected.", actualWtFwpByteArray6Size,
|
||||
wtFwpByteArray6_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpByteArray16Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpByteArray16Size = unsafe.Sizeof(wtFwpByteArray16{})
|
||||
|
||||
if actualWtFwpByteArray16Size != wtFwpByteArray16_Size {
|
||||
t.Errorf("Size of wtFwpByteArray16 is %d, although %d is expected.", actualWtFwpByteArray16Size,
|
||||
wtFwpByteArray16_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpConditionValue0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpConditionValue0Size = unsafe.Sizeof(wtFwpConditionValue0{})
|
||||
|
||||
if actualWtFwpConditionValue0Size != wtFwpConditionValue0_Size {
|
||||
t.Errorf("Size of wtFwpConditionValue0 is %d, although %d is expected.", actualWtFwpConditionValue0Size,
|
||||
wtFwpConditionValue0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpConditionValue0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpConditionValue0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.value)) - sp
|
||||
|
||||
if offset != wtFwpConditionValue0_uint8_Offset {
|
||||
t.Errorf("wtFwpConditionValue0.value offset is %d although %d is expected", offset, wtFwpConditionValue0_uint8_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpV4AddrAndMaskSize(t *testing.T) {
|
||||
|
||||
const actualWtFwpV4AddrAndMaskSize = unsafe.Sizeof(wtFwpV4AddrAndMask{})
|
||||
|
||||
if actualWtFwpV4AddrAndMaskSize != wtFwpV4AddrAndMask_Size {
|
||||
t.Errorf("Size of wtFwpV4AddrAndMask is %d, although %d is expected.", actualWtFwpV4AddrAndMaskSize,
|
||||
wtFwpV4AddrAndMask_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpV4AddrAndMaskOffsets(t *testing.T) {
|
||||
|
||||
s := wtFwpV4AddrAndMask{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.mask)) - sp
|
||||
|
||||
if offset != wtFwpV4AddrAndMask_mask_Offset {
|
||||
t.Errorf("wtFwpV4AddrAndMask.mask offset is %d although %d is expected", offset,
|
||||
wtFwpV4AddrAndMask_mask_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpV6AddrAndMaskSize(t *testing.T) {
|
||||
|
||||
const actualWtFwpV6AddrAndMaskSize = unsafe.Sizeof(wtFwpV6AddrAndMask{})
|
||||
|
||||
if actualWtFwpV6AddrAndMaskSize != wtFwpV6AddrAndMask_Size {
|
||||
t.Errorf("Size of wtFwpV6AddrAndMask is %d, although %d is expected.", actualWtFwpV6AddrAndMaskSize,
|
||||
wtFwpV6AddrAndMask_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpV6AddrAndMaskOffsets(t *testing.T) {
|
||||
|
||||
s := wtFwpV6AddrAndMask{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.prefixLength)) - sp
|
||||
|
||||
if offset != wtFwpV6AddrAndMask_prefixLength_Offset {
|
||||
t.Errorf("wtFwpV6AddrAndMask.prefixLength offset is %d although %d is expected", offset,
|
||||
wtFwpV6AddrAndMask_prefixLength_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpValue0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpValue0Size = unsafe.Sizeof(wtFwpValue0{})
|
||||
|
||||
if actualWtFwpValue0Size != wtFwpValue0_Size {
|
||||
t.Errorf("Size of wtFwpValue0 is %d, although %d is expected.", actualWtFwpValue0Size, wtFwpValue0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpValue0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpValue0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.value)) - sp
|
||||
|
||||
if offset != wtFwpValue0_value_Offset {
|
||||
t.Errorf("wtFwpValue0.value offset is %d although %d is expected", offset, wtFwpValue0_value_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmDisplayData0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpmDisplayData0Size = unsafe.Sizeof(wtFwpmDisplayData0{})
|
||||
|
||||
if actualWtFwpmDisplayData0Size != wtFwpmDisplayData0_Size {
|
||||
t.Errorf("Size of wtFwpmDisplayData0 is %d, although %d is expected.", actualWtFwpmDisplayData0Size,
|
||||
wtFwpmDisplayData0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmDisplayData0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpmDisplayData0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.description)) - sp
|
||||
|
||||
if offset != wtFwpmDisplayData0_description_Offset {
|
||||
t.Errorf("wtFwpmDisplayData0.description offset is %d although %d is expected", offset,
|
||||
wtFwpmDisplayData0_description_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmFilterCondition0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpmFilterCondition0Size = unsafe.Sizeof(wtFwpmFilterCondition0{})
|
||||
|
||||
if actualWtFwpmFilterCondition0Size != wtFwpmFilterCondition0_Size {
|
||||
t.Errorf("Size of wtFwpmFilterCondition0 is %d, although %d is expected.",
|
||||
actualWtFwpmFilterCondition0Size, wtFwpmFilterCondition0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmFilterCondition0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpmFilterCondition0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.matchType)) - sp
|
||||
|
||||
if offset != wtFwpmFilterCondition0_matchType_Offset {
|
||||
t.Errorf("wtFwpmFilterCondition0.matchType offset is %d although %d is expected", offset,
|
||||
wtFwpmFilterCondition0_matchType_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.conditionValue)) - sp
|
||||
|
||||
if offset != wtFwpmFilterCondition0_conditionValue_Offset {
|
||||
t.Errorf("wtFwpmFilterCondition0.conditionValue offset is %d although %d is expected", offset,
|
||||
wtFwpmFilterCondition0_conditionValue_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmFilter0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpmFilter0Size = unsafe.Sizeof(wtFwpmFilter0{})
|
||||
|
||||
if actualWtFwpmFilter0Size != wtFwpmFilter0_Size {
|
||||
t.Errorf("Size of wtFwpmFilter0 is %d, although %d is expected.", actualWtFwpmFilter0Size,
|
||||
wtFwpmFilter0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmFilter0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpmFilter0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.displayData)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_displayData_Offset {
|
||||
t.Errorf("wtFwpmFilter0.displayData offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_displayData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.flags)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_flags_Offset {
|
||||
t.Errorf("wtFwpmFilter0.flags offset is %d although %d is expected", offset, wtFwpmFilter0_flags_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.providerKey)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_providerKey_Offset {
|
||||
t.Errorf("wtFwpmFilter0.providerKey offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_providerKey_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.providerData)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_providerData_Offset {
|
||||
t.Errorf("wtFwpmFilter0.providerData offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_providerData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.layerKey)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_layerKey_Offset {
|
||||
t.Errorf("wtFwpmFilter0.layerKey offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_layerKey_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.subLayerKey)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_subLayerKey_Offset {
|
||||
t.Errorf("wtFwpmFilter0.subLayerKey offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_subLayerKey_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.weight)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_weight_Offset {
|
||||
t.Errorf("wtFwpmFilter0.weight offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_weight_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.numFilterConditions)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_numFilterConditions_Offset {
|
||||
t.Errorf("wtFwpmFilter0.numFilterConditions offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_numFilterConditions_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.filterCondition)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_filterCondition_Offset {
|
||||
t.Errorf("wtFwpmFilter0.filterCondition offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_filterCondition_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.action)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_action_Offset {
|
||||
t.Errorf("wtFwpmFilter0.action offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_action_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.providerContextKey)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_providerContextKey_Offset {
|
||||
t.Errorf("wtFwpmFilter0.providerContextKey offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_providerContextKey_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.reserved)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_reserved_Offset {
|
||||
t.Errorf("wtFwpmFilter0.reserved offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_reserved_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.filterID)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_filterID_Offset {
|
||||
t.Errorf("wtFwpmFilter0.filterID offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_filterID_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.effectiveWeight)) - sp
|
||||
|
||||
if offset != wtFwpmFilter0_effectiveWeight_Offset {
|
||||
t.Errorf("wtFwpmFilter0.effectiveWeight offset is %d although %d is expected", offset,
|
||||
wtFwpmFilter0_effectiveWeight_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpProvider0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpProvider0Size = unsafe.Sizeof(wtFwpProvider0{})
|
||||
|
||||
if actualWtFwpProvider0Size != wtFwpProvider0_Size {
|
||||
t.Errorf("Size of wtFwpProvider0 is %d, although %d is expected.", actualWtFwpProvider0Size,
|
||||
wtFwpProvider0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpProvider0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpProvider0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.displayData)) - sp
|
||||
|
||||
if offset != wtFwpProvider0_displayData_Offset {
|
||||
t.Errorf("wtFwpProvider0.displayData offset is %d although %d is expected", offset,
|
||||
wtFwpProvider0_displayData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.flags)) - sp
|
||||
|
||||
if offset != wtFwpProvider0_flags_Offset {
|
||||
t.Errorf("wtFwpProvider0.flags offset is %d although %d is expected", offset,
|
||||
wtFwpProvider0_flags_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.providerData)) - sp
|
||||
|
||||
if offset != wtFwpProvider0_providerData_Offset {
|
||||
t.Errorf("wtFwpProvider0.providerData offset is %d although %d is expected", offset,
|
||||
wtFwpProvider0_providerData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.serviceName)) - sp
|
||||
|
||||
if offset != wtFwpProvider0_serviceName_Offset {
|
||||
t.Errorf("wtFwpProvider0.serviceName offset is %d although %d is expected", offset,
|
||||
wtFwpProvider0_serviceName_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmSession0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpmSession0Size = unsafe.Sizeof(wtFwpmSession0{})
|
||||
|
||||
if actualWtFwpmSession0Size != wtFwpmSession0_Size {
|
||||
t.Errorf("Size of wtFwpmSession0 is %d, although %d is expected.", actualWtFwpmSession0Size,
|
||||
wtFwpmSession0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmSession0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpmSession0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.displayData)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_displayData_Offset {
|
||||
t.Errorf("wtFwpmSession0.displayData offset is %d although %d is expected", offset,
|
||||
wtFwpmSession0_displayData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.flags)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_flags_Offset {
|
||||
t.Errorf("wtFwpmSession0.flags offset is %d although %d is expected", offset, wtFwpmSession0_flags_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.txnWaitTimeoutInMSec)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_txnWaitTimeoutInMSec_Offset {
|
||||
t.Errorf("wtFwpmSession0.txnWaitTimeoutInMSec offset is %d although %d is expected", offset,
|
||||
wtFwpmSession0_txnWaitTimeoutInMSec_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.processId)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_processId_Offset {
|
||||
t.Errorf("wtFwpmSession0.processId offset is %d although %d is expected", offset,
|
||||
wtFwpmSession0_processId_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.sid)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_sid_Offset {
|
||||
t.Errorf("wtFwpmSession0.sid offset is %d although %d is expected", offset, wtFwpmSession0_sid_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.username)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_username_Offset {
|
||||
t.Errorf("wtFwpmSession0.username offset is %d although %d is expected", offset,
|
||||
wtFwpmSession0_username_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.kernelMode)) - sp
|
||||
|
||||
if offset != wtFwpmSession0_kernelMode_Offset {
|
||||
t.Errorf("wtFwpmSession0.kernelMode offset is %d although %d is expected", offset,
|
||||
wtFwpmSession0_kernelMode_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmSublayer0Size(t *testing.T) {
|
||||
|
||||
const actualWtFwpmSublayer0Size = unsafe.Sizeof(wtFwpmSublayer0{})
|
||||
|
||||
if actualWtFwpmSublayer0Size != wtFwpmSublayer0_Size {
|
||||
t.Errorf("Size of wtFwpmSublayer0 is %d, although %d is expected.", actualWtFwpmSublayer0Size,
|
||||
wtFwpmSublayer0_Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWtFwpmSublayer0Offsets(t *testing.T) {
|
||||
|
||||
s := wtFwpmSublayer0{}
|
||||
sp := uintptr(unsafe.Pointer(&s))
|
||||
|
||||
offset := uintptr(unsafe.Pointer(&s.displayData)) - sp
|
||||
|
||||
if offset != wtFwpmSublayer0_displayData_Offset {
|
||||
t.Errorf("wtFwpmSublayer0.displayData offset is %d although %d is expected", offset,
|
||||
wtFwpmSublayer0_displayData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.flags)) - sp
|
||||
|
||||
if offset != wtFwpmSublayer0_flags_Offset {
|
||||
t.Errorf("wtFwpmSublayer0.flags offset is %d although %d is expected", offset,
|
||||
wtFwpmSublayer0_flags_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.providerKey)) - sp
|
||||
|
||||
if offset != wtFwpmSublayer0_providerKey_Offset {
|
||||
t.Errorf("wtFwpmSublayer0.providerKey offset is %d although %d is expected", offset,
|
||||
wtFwpmSublayer0_providerKey_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.providerData)) - sp
|
||||
|
||||
if offset != wtFwpmSublayer0_providerData_Offset {
|
||||
t.Errorf("wtFwpmSublayer0.providerData offset is %d although %d is expected", offset,
|
||||
wtFwpmSublayer0_providerData_Offset)
|
||||
return
|
||||
}
|
||||
|
||||
offset = uintptr(unsafe.Pointer(&s.weight)) - sp
|
||||
|
||||
if offset != wtFwpmSublayer0_weight_Offset {
|
||||
t.Errorf("wtFwpmSublayer0.weight offset is %d although %d is expected", offset,
|
||||
wtFwpmSublayer0_weight_Offset)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modfwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll")
|
||||
|
||||
procFwpmEngineClose0 = modfwpuclnt.NewProc("FwpmEngineClose0")
|
||||
procFwpmEngineOpen0 = modfwpuclnt.NewProc("FwpmEngineOpen0")
|
||||
procFwpmFilterAdd0 = modfwpuclnt.NewProc("FwpmFilterAdd0")
|
||||
procFwpmFreeMemory0 = modfwpuclnt.NewProc("FwpmFreeMemory0")
|
||||
procFwpmGetAppIdFromFileName0 = modfwpuclnt.NewProc("FwpmGetAppIdFromFileName0")
|
||||
procFwpmProviderAdd0 = modfwpuclnt.NewProc("FwpmProviderAdd0")
|
||||
procFwpmSubLayerAdd0 = modfwpuclnt.NewProc("FwpmSubLayerAdd0")
|
||||
procFwpmTransactionAbort0 = modfwpuclnt.NewProc("FwpmTransactionAbort0")
|
||||
procFwpmTransactionBegin0 = modfwpuclnt.NewProc("FwpmTransactionBegin0")
|
||||
procFwpmTransactionCommit0 = modfwpuclnt.NewProc("FwpmTransactionCommit0")
|
||||
)
|
||||
|
||||
func fwpmEngineClose0(engineHandle uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmEngineClose0.Addr(), 1, uintptr(engineHandle), 0, 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procFwpmEngineOpen0.Addr(), 5, uintptr(unsafe.Pointer(serverName)), uintptr(authnService), uintptr(unsafe.Pointer(authIdentity)), uintptr(unsafe.Pointer(session)), uintptr(engineHandle), 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procFwpmFilterAdd0.Addr(), 4, uintptr(engineHandle), uintptr(unsafe.Pointer(filter)), uintptr(sd), uintptr(unsafe.Pointer(id)), 0, 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmFreeMemory0(p unsafe.Pointer) {
|
||||
syscall.Syscall(procFwpmFreeMemory0.Addr(), 1, uintptr(p), 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmGetAppIdFromFileName0.Addr(), 2, uintptr(unsafe.Pointer(fileName)), uintptr(appID), 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmProviderAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(provider)), uintptr(sd))
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmSubLayerAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(subLayer)), uintptr(sd))
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmTransactionAbort0(engineHandle uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmTransactionAbort0.Addr(), 1, uintptr(engineHandle), 0, 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmTransactionBegin0.Addr(), 2, uintptr(engineHandle), uintptr(flags), 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fwpmTransactionCommit0(engineHandle uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFwpmTransactionCommit0.Addr(), 1, uintptr(engineHandle), 0, 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -34,7 +34,7 @@ type Packet struct {
|
||||
Payload []byte
|
||||
|
||||
// Prefix set by various internal methods of natlab, to locate
|
||||
// where in the network a trace occured.
|
||||
// where in the network a trace occurred.
|
||||
locator string
|
||||
}
|
||||
|
||||
|
||||
259
tsweb/tsweb.go
259
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() }))
|
||||
}
|
||||
@@ -83,7 +87,7 @@ func AllowDebugAccess(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// Protected wraps a provided debug handler, h, returning a Handler
|
||||
// that enforces AllowDebugAccess and returns forbiden replies for
|
||||
// that enforces AllowDebugAccess and returns forbidden replies for
|
||||
// unauthorized requests.
|
||||
func Protected(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -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.
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
)
|
||||
|
||||
// There is much overlap between the theory of serialization and hashing.
|
||||
// A hash (useful for determing equality) can be produced by printing a value
|
||||
// A hash (useful for determining equality) can be produced by printing a value
|
||||
// and hashing the output. The format must:
|
||||
// * be deterministic such that the same value hashes to the same output, and
|
||||
// * be parsable such that the same value can be reproduced by the output.
|
||||
|
||||
@@ -15,7 +15,7 @@ var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||
|
||||
// IsMemberOfGroup verifies if the provided user is member of the provided
|
||||
// system group.
|
||||
// If verfication fails, an error is returned.
|
||||
// If verification fails, an error is returned.
|
||||
func IsMemberOfGroup(group, userName string) (bool, error) {
|
||||
return isMemberOfGroup(group, userName)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
255
wgengine/kernel.go
Normal file
255
wgengine/kernel.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// 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 wgengine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/kproxy"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
type kernelEngine struct {
|
||||
logf logger.Logf
|
||||
magicConn *magicsock.Conn
|
||||
linkMon *monitor.Mon
|
||||
linkMonOwned bool // whether we created linkMon (and thus need to close it)
|
||||
router router.Router
|
||||
dns *dns.Manager
|
||||
confListenPort uint16 // original conf.ListenPort
|
||||
wg *wgctrl.Client
|
||||
proxy *kproxy.Proxy
|
||||
proxyMap map[tailcfg.NodeKey]netaddr.IPPort
|
||||
|
||||
wgLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewKernelEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) {
|
||||
var closePool closeOnErrorPool
|
||||
defer closePool.closeAllIfError(&reterr)
|
||||
|
||||
const tunName = "tailscale0" // TODO: plumb somehow for variable name
|
||||
|
||||
if conf.Tun != nil {
|
||||
return nil, errors.New("can't use a tun interface in kernel mode")
|
||||
}
|
||||
if conf.Router == nil {
|
||||
conf.Router = router.NewFake(logf)
|
||||
}
|
||||
if conf.DNS == nil {
|
||||
d, err := dns.NewNoopManager()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.DNS = d
|
||||
}
|
||||
|
||||
e := &kernelEngine{
|
||||
logf: logf,
|
||||
router: conf.Router,
|
||||
confListenPort: conf.ListenPort,
|
||||
}
|
||||
if conf.LinkMonitor != nil {
|
||||
e.linkMon = conf.LinkMonitor
|
||||
} else {
|
||||
mon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
closePool.add(mon)
|
||||
e.linkMon = mon
|
||||
e.linkMonOwned = true
|
||||
}
|
||||
e.dns = dns.NewManager(logf, conf.DNS, e.linkMon, nil) // TODO: make a fwdLinkSelector
|
||||
|
||||
magicsockOpts := magicsock.Options{
|
||||
Logf: logf,
|
||||
Port: conf.ListenPort,
|
||||
LinkMonitor: e.linkMon,
|
||||
}
|
||||
|
||||
var err error
|
||||
e.magicConn, err = magicsock.NewConn(magicsockOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgengine: %v", err)
|
||||
}
|
||||
closePool.add(e.magicConn)
|
||||
e.magicConn.SetNetworkUp(true)
|
||||
|
||||
e.proxy, err = kproxy.New(e.magicConn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy: %v", err)
|
||||
}
|
||||
|
||||
e.wg, err = wgctrl.New()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgctrl: %v", err)
|
||||
}
|
||||
closePool.add(e.wg)
|
||||
|
||||
err = e.wg.ConfigureDevice(tunName, wgtypes.Config{
|
||||
PrivateKey: &wgtypes.Key{},
|
||||
ReplacePeers: true,
|
||||
Peers: []wgtypes.PeerConfig{},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgctrl: initial config: %v", err)
|
||||
}
|
||||
|
||||
if err := e.router.Up(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.magicConn.Start()
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *kernelEngine) Reconfig(wcfg *wgcfg.Config, rcfg *router.Config, dcfg *dns.Config, dbg *tailcfg.Debug) error {
|
||||
if rcfg == nil {
|
||||
panic("rcfg must not be nil")
|
||||
}
|
||||
if dcfg == nil {
|
||||
panic("dcfg must not be nil")
|
||||
}
|
||||
|
||||
e.wgLock.Lock()
|
||||
defer e.wgLock.Unlock()
|
||||
|
||||
peerSet := make(map[key.Public]struct{}, len(wcfg.Peers))
|
||||
for _, p := range wcfg.Peers {
|
||||
peerSet[key.Public(p.PublicKey)] = struct{}{}
|
||||
}
|
||||
|
||||
if err := e.magicConn.SetPrivateKey(wgkey.Private(wcfg.PrivateKey)); err != nil {
|
||||
e.logf("wgengine: Reconfig: SetPrivateKey: %v", err)
|
||||
}
|
||||
e.magicConn.UpdatePeers(peerSet)
|
||||
e.magicConn.SetPreferredPort(e.confListenPort)
|
||||
|
||||
port := 4242
|
||||
cfg := wgtypes.Config{
|
||||
PrivateKey: (*wgtypes.Key)(&wcfg.PrivateKey),
|
||||
ListenPort: &port,
|
||||
ReplacePeers: true,
|
||||
}
|
||||
for _, p := range wcfg.Peers {
|
||||
v := wgtypes.PeerConfig{
|
||||
PublicKey: wgtypes.Key(p.PublicKey),
|
||||
Endpoint: e.proxyMap[tailcfg.NodeKey(p.PublicKey)].UDPAddr(),
|
||||
ReplaceAllowedIPs: true,
|
||||
}
|
||||
for _, pfx := range p.AllowedIPs {
|
||||
v.AllowedIPs = append(v.AllowedIPs, *pfx.IPNet())
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, v)
|
||||
}
|
||||
if err := e.wg.ConfigureDevice("tailscale0", cfg); err != nil {
|
||||
return fmt.Errorf("configuring kernel: %v", err)
|
||||
}
|
||||
|
||||
err := e.router.Set(rcfg)
|
||||
health.SetRouterHealth(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: set DNS, but it'll just break my machine right now, I
|
||||
// mean look at the state of me.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *kernelEngine) GetFilter() *filter.Filter { return nil }
|
||||
|
||||
func (e *kernelEngine) SetFilter(f *filter.Filter) {}
|
||||
|
||||
func (e *kernelEngine) SetStatusCallback(cb StatusCallback) {}
|
||||
|
||||
func (e *kernelEngine) GetLinkMonitor() *monitor.Mon { return e.linkMon }
|
||||
|
||||
func (e *kernelEngine) RequestStatus() {}
|
||||
|
||||
func (e *kernelEngine) Close() {}
|
||||
|
||||
func (e *kernelEngine) Wait() {}
|
||||
|
||||
func (e *kernelEngine) LinkChange(isExpensive bool) {}
|
||||
|
||||
func (e *kernelEngine) SetDERPMap(m *tailcfg.DERPMap) {
|
||||
e.magicConn.SetDERPMap(m)
|
||||
}
|
||||
|
||||
func (e *kernelEngine) SetNetworkMap(nm *netmap.NetworkMap) {
|
||||
e.magicConn.SetNetworkMap(nm)
|
||||
m, err := e.proxy.SetNetworkMap(nm)
|
||||
if err != nil {
|
||||
e.logf("MUCH SADNESS: %v", err)
|
||||
}
|
||||
e.wgLock.Lock()
|
||||
defer e.wgLock.Unlock()
|
||||
e.proxyMap = m
|
||||
}
|
||||
|
||||
func (e *kernelEngine) AddNetworkMapCallback(cb NetworkMapCallback) (rm func()) { return func() {} }
|
||||
|
||||
func (e *kernelEngine) SetNetInfoCallback(cb NetInfoCallback) {}
|
||||
|
||||
func (e *kernelEngine) DiscoPublicKey() tailcfg.DiscoKey { return e.magicConn.DiscoPublicKey() }
|
||||
|
||||
func (e *kernelEngine) getStatus() (*Status, error) {
|
||||
// Grab derpConns before acquiring wgLock to not violate lock ordering;
|
||||
// the DERPs method acquires magicsock.Conn.mu.
|
||||
// (See comment in userspaceEngine's declaration.)
|
||||
derpConns := e.magicConn.DERPs()
|
||||
|
||||
return &Status{
|
||||
LocalAddrs: []tailcfg.Endpoint{},
|
||||
Peers: []ipnstate.PeerStatusLite{},
|
||||
DERPs: derpConns,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *kernelEngine) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
st, err := e.getStatus()
|
||||
if err != nil {
|
||||
e.logf("wgengine: getStatus: %v", err)
|
||||
return
|
||||
}
|
||||
for _, ps := range st.Peers {
|
||||
sb.AddPeer(key.Public(ps.NodeKey), &ipnstate.PeerStatus{
|
||||
RxBytes: int64(ps.RxBytes),
|
||||
TxBytes: int64(ps.TxBytes),
|
||||
LastHandshake: ps.LastHandshake,
|
||||
InEngine: true,
|
||||
})
|
||||
}
|
||||
|
||||
e.magicConn.UpdateStatus(sb)
|
||||
}
|
||||
|
||||
func (e *kernelEngine) Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult)) {}
|
||||
|
||||
func (e *kernelEngine) RegisterIPPortIdentity(ipp netaddr.IPPort, ip netaddr.IP) {}
|
||||
|
||||
func (e *kernelEngine) UnregisterIPPortIdentity(ipp netaddr.IPPort) {}
|
||||
|
||||
func (e *kernelEngine) WhoIsIPPort(netaddr.IPPort) (netaddr.IP, bool) { return netaddr.IP{}, false }
|
||||
136
wgengine/kproxy/proxy.go
Normal file
136
wgengine/kproxy/proxy.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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 kproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"golang.zx2c4.com/wireguard/conn"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
mu sync.RWMutex
|
||||
conn *magicsock.Conn
|
||||
byKey map[tailcfg.NodeKey]*pipe
|
||||
byEndpoint map[conn.Endpoint]*pipe
|
||||
}
|
||||
|
||||
func New(c *magicsock.Conn) (*Proxy, error) {
|
||||
ret := &Proxy{
|
||||
conn: c,
|
||||
byEndpoint: map[conn.Endpoint]*pipe{},
|
||||
byKey: map[tailcfg.NodeKey]*pipe{},
|
||||
}
|
||||
fns, _, err := c.Bind().Open(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, fn := range fns {
|
||||
go func(fn conn.ReceiveFunc) {
|
||||
for {
|
||||
var bs [1500]byte
|
||||
n, ep, err := fn(bs[:])
|
||||
if err != nil {
|
||||
// Sadness.
|
||||
continue
|
||||
}
|
||||
ret.mu.RLock()
|
||||
pip, ok := ret.byEndpoint[ep]
|
||||
ret.mu.RUnlock()
|
||||
if ok {
|
||||
if _, err := pip.proxy.Write(bs[:n]); err != nil {
|
||||
_ = err // TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
}(fn)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var proxyListenIP = netaddr.MustParseIPPort("127.0.0.1:0")
|
||||
var wgIP = netaddr.MustParseIPPort("127.0.0.1:4242")
|
||||
|
||||
func (p *Proxy) SetNetworkMap(nm *netmap.NetworkMap) (map[tailcfg.NodeKey]netaddr.IPPort, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
ret := make(map[tailcfg.NodeKey]netaddr.IPPort, len(nm.Peers))
|
||||
for _, peer := range nm.Peers {
|
||||
if pip, ok := p.byKey[peer.Key]; ok {
|
||||
ret[peer.Key] = pip.proxyAddr
|
||||
} else {
|
||||
wgEp := wgcfg.Endpoints{
|
||||
PublicKey: wgkey.Key(peer.Key),
|
||||
DiscoKey: peer.DiscoKey,
|
||||
}
|
||||
bs, err := json.Marshal(wgEp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ep, err := p.conn.ParseEndpoint(string(bs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := net.DialUDP("udp4", proxyListenIP.UDPAddr(), wgIP.UDPAddr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connAddr := netaddr.MustParseIPPort(conn.LocalAddr().String())
|
||||
pip = &pipe{
|
||||
ep: ep,
|
||||
proxy: conn,
|
||||
proxyAddr: connAddr,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
var bs [1500]byte
|
||||
n, ua, err := conn.ReadFromUDP(bs[:])
|
||||
if err != nil {
|
||||
return // TODO: more noise
|
||||
}
|
||||
ip, ok := netaddr.FromStdIP(ua.IP)
|
||||
if !ok {
|
||||
// ???
|
||||
continue
|
||||
}
|
||||
if netaddr.IPPortFrom(ip, uint16(ua.Port)) != wgIP {
|
||||
// Random noise that isn't kernel wg
|
||||
continue
|
||||
}
|
||||
if err := p.conn.Send(bs[:n], ep); err != nil {
|
||||
// Probably complain a bit
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
p.byKey[peer.Key] = pip
|
||||
p.byEndpoint[ep] = pip
|
||||
ret[peer.Key] = pip.proxyAddr
|
||||
}
|
||||
}
|
||||
for key, pip := range p.byKey {
|
||||
if _, ok := ret[key]; !ok {
|
||||
pip.proxy.Close()
|
||||
delete(p.byKey, key)
|
||||
delete(p.byEndpoint, pip.ep)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type pipe struct {
|
||||
ep conn.Endpoint
|
||||
proxy net.Conn
|
||||
proxyAddr netaddr.IPPort
|
||||
}
|
||||
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,24 +2766,53 @@ 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)
|
||||
discoKey: tailcfg.DiscoKey(discoKey), // for discovery mesages
|
||||
discoKey: tailcfg.DiscoKey(discoKey), // for discovery messages
|
||||
discoShort: tailcfg.DiscoKey(discoKey).ShortString(),
|
||||
wgEndpoint: endpointStr,
|
||||
sentPing: map[stun.TxID]sentPing{},
|
||||
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
|
||||
}
|
||||
@@ -3133,7 +3128,7 @@ type discoEndpoint struct {
|
||||
// These fields are initialized once and never modified.
|
||||
c *Conn
|
||||
publicKey tailcfg.NodeKey // peer public key (for WireGuard + DERP)
|
||||
discoKey tailcfg.DiscoKey // for discovery mesages
|
||||
discoKey tailcfg.DiscoKey // for discovery messages
|
||||
discoShort string // ShortString of discoKey
|
||||
fakeWGAddr netaddr.IPPort // the UDP address we tell wireguard-go we're using
|
||||
wgEndpoint string // string from ParseEndpoint, holds a JSON-serialized wgcfg.Endpoints
|
||||
@@ -3290,7 +3285,7 @@ func (de *discoEndpoint) initFakeUDPAddr() {
|
||||
de.fakeWGAddr = netaddr.IPPortFrom(netaddr.IPFrom16(addr), 12345)
|
||||
}
|
||||
|
||||
// isFirstRecvActivityInAwhile notes that receive activity has occured for this
|
||||
// isFirstRecvActivityInAwhile notes that receive activity has occurred for this
|
||||
// endpoint and reports whether it's been at least 10 seconds since the last
|
||||
// receive activity (including having never received from this peer before).
|
||||
func (de *discoEndpoint) isFirstRecvActivityInAwhile() bool {
|
||||
|
||||
@@ -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,33 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
return
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
// The ForwarderRequest.CreateEndpoint above asynchronously
|
||||
// starts 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
|
||||
// gonet.TCPConn.RemoteAddr. The byte copies in both
|
||||
// directions to/from the gonet.TCPConn 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 +560,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)
|
||||
|
||||
@@ -125,10 +125,11 @@ type linuxRouter struct {
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, linkMon *monitor.Mon) (Router, error) {
|
||||
tunname, err := tunDev.Name()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tunname := "tailscale0"
|
||||
// , err := tunDev.Name()
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
@@ -155,7 +156,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)
|
||||
}
|
||||
|
||||
@@ -108,6 +108,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
|
||||
@@ -606,7 +607,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 +651,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 +923,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 +956,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 +987,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 +997,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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