Compare commits
1 Commits
crawshaw/l
...
bradfitz/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1024a5de2 |
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/license.yml
vendored
2
.github/workflows/license.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/staticcheck.yml
vendored
2
.github/workflows/staticcheck.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.14
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
5
Makefile
5
Makefile
@@ -1,10 +1,7 @@
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
check: staticcheck vet
|
||||
check: staticcheck
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -31,7 +31,7 @@ go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.15) in module mode. It might
|
||||
release candidate builds (currently Go 1.14) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
@@ -186,15 +185,6 @@ func main() {
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
|
||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := letsEncryptGetCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
go func() {
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
|
||||
if err != nil {
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -89,16 +88,7 @@ func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
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)
|
||||
}
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
default:
|
||||
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ func ActLikeCLI() bool {
|
||||
return false
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
case "up", "status", "netcheck", "version",
|
||||
"-V", "--version", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
@@ -58,21 +57,14 @@ change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
netcheckCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -134,12 +126,3 @@ func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,175 +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.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func() {
|
||||
st, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
log.Printf("error getting state: %v", err)
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump()
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
dmap := derpmap.Prod()
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
@@ -1,67 +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.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
Name: "down",
|
||||
ShortUsage: "down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
timer := time.AfterFunc(5*time.Second, func() {
|
||||
log.Fatalf("timeout running stop")
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
cur := n.Status.BackendState
|
||||
switch cur {
|
||||
case "Stopped":
|
||||
log.Printf("already stopped")
|
||||
cancel()
|
||||
default:
|
||||
log.Printf("was in state %q", cur)
|
||||
}
|
||||
return
|
||||
}
|
||||
if n.State != nil {
|
||||
log.Printf("now in state %q", *n.State)
|
||||
if *n.State == ipn.Stopped {
|
||||
cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("Notify: %#v", n)
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
bc.SetWantRunning(false)
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -125,35 +125,20 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
if len(report.RegionLatency) == 0 {
|
||||
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
fmt.Printf("\t* Nearest DERP: %v (%v)\n", report.PreferredDERP, dm.Regions[report.PreferredDERP].RegionCode)
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
var rids []int
|
||||
for rid := range dm.Regions {
|
||||
rids = append(rids, rid)
|
||||
}
|
||||
sort.Slice(rids, func(i, j int) bool {
|
||||
l1, ok1 := report.RegionLatency[rids[i]]
|
||||
l2, ok2 := report.RegionLatency[rids[j]]
|
||||
if ok1 != ok2 {
|
||||
return ok1 // defined things sort first
|
||||
}
|
||||
if !ok1 {
|
||||
return rids[i] < rids[j]
|
||||
}
|
||||
return l1 < l2
|
||||
})
|
||||
sort.Ints(rids)
|
||||
for _, rid := range rids {
|
||||
d, ok := report.RegionLatency[rid]
|
||||
var latency string
|
||||
if ok {
|
||||
latency = d.Round(time.Millisecond / 10).String()
|
||||
}
|
||||
r := dm.Regions[rid]
|
||||
var derpNum string
|
||||
if netcheckArgs.verbose {
|
||||
derpNum = fmt.Sprintf("derp%d, ", rid)
|
||||
}
|
||||
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
fmt.Printf("\t\t- %v, %3s = %s\n", rid, dm.Regions[rid].RegionCode, latency)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,130 +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.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var pingCmd = &ffcli.Command{
|
||||
Name: "ping",
|
||||
ShortUsage: "ping <hostname-or-IP>",
|
||||
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
The 'tailscale ping' command pings a peer node at the Tailscale layer
|
||||
and reports which route it took for each response. The first ping or
|
||||
so will likely go over DERP (Tailscale's TCP relay protocol) while NAT
|
||||
traversal finds a direct path through.
|
||||
|
||||
If 'tailscale ping' works but a normal ping does not, that means one
|
||||
side's operating system firewall is blocking packets; 'tailscale ping'
|
||||
does not inject packets into either side's TUN devices.
|
||||
|
||||
By default, 'tailscale ping' stops after 10 pings or once a direct
|
||||
(non-DERP) path has been established, whichever comes first.
|
||||
|
||||
The provided hostname must resolve to or be a Tailscale IP
|
||||
(e.g. 100.x.y.z) or a subnet IP advertised by a Tailscale
|
||||
relay node.
|
||||
|
||||
`),
|
||||
Exec: runPing,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ping", flag.ExitOnError)
|
||||
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var pingArgs struct {
|
||||
num int
|
||||
untilDirect bool
|
||||
verbose bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
ch <- pr
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
n++
|
||||
bc.Ping(ip)
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case pr := <-ch:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
}
|
||||
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
|
||||
via := pr.Endpoint
|
||||
if pr.DERPRegionID != 0 {
|
||||
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
|
||||
}
|
||||
anyPong = true
|
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
|
||||
if pr.Endpoint != "" && pingArgs.untilDirect {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if n == pingArgs.num {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ var statusCmd = &ffcli.Command{
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
@@ -46,7 +45,6 @@ var statusArgs struct {
|
||||
listen string // in web mode, webserver address to listen on, empty means auto
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
self bool // in CLI mode, show status of local machine
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
@@ -127,17 +125,16 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if st.BackendState == ipn.Stopped.String() {
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
ps.PublicKey.ShortString(),
|
||||
peer.ShortString(),
|
||||
ps.OS,
|
||||
ps.TailAddr,
|
||||
ps.SimpleHostName(),
|
||||
@@ -163,18 +160,6 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
@@ -54,7 +53,6 @@ specify any flags, options are reset to their default.
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
@@ -77,7 +75,6 @@ var upArgs struct {
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
enableDERP bool
|
||||
@@ -184,6 +181,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
// TODO(apenwarr): allow setting/using CorpDNS
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
@@ -215,8 +213,6 @@ func runUp(ctx context.Context, args []string) error {
|
||||
defer cancel()
|
||||
|
||||
var printed bool
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
opts := ipn.Options{
|
||||
@@ -230,7 +226,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
bc.StartLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
@@ -256,10 +252,6 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// ephemeral frontends that read/modify/write state, once
|
||||
// Windows/Mac state is moved into backend.
|
||||
bc.Start(opts)
|
||||
if upArgs.forceReauth {
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
}
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
@@ -36,10 +35,8 @@ import (
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -148,8 +145,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
if httpc == nil {
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dialer.DialContext
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
@@ -626,12 +621,8 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
vlogf("netmap: new map contains DERP map")
|
||||
lastDERPMap = resp.DERPMap
|
||||
}
|
||||
if resp.Debug != nil {
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
if resp.Debug != nil && resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
// Temporarily (2020-06-29) support removing all but
|
||||
// discovery-supporting nodes during development, for
|
||||
@@ -962,30 +953,3 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
}
|
||||
return v2
|
||||
}
|
||||
|
||||
// opt.Bool configs from control.
|
||||
var (
|
||||
controlUseDERPRoute atomic.Value
|
||||
controlTrimWGConfig atomic.Value
|
||||
)
|
||||
|
||||
func setControlAtomic(dst *atomic.Value, v opt.Bool) {
|
||||
old, ok := dst.Load().(opt.Bool)
|
||||
if !ok || old != v {
|
||||
dst.Store(v)
|
||||
}
|
||||
}
|
||||
|
||||
// DERPRouteFlag reports the last reported value from control for whether
|
||||
// DERP route optimization (Issue 150) should be enabled.
|
||||
func DERPRouteFlag() opt.Bool {
|
||||
v, _ := controlUseDERPRoute.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// TrimWGConfig reports the last reported value from control for whether
|
||||
// we should do lazy wireguard configuration.
|
||||
func TrimWGConfig() opt.Bool {
|
||||
v, _ := controlTrimWGConfig.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -5,16 +5,80 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
func parseIP(host string, defaultBits int) (filter.Net, error) {
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil && ip.IsUnspecified() {
|
||||
// For clarity, reject 0.0.0.0 as an input
|
||||
return filter.NetNone, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host)
|
||||
} else if ip == nil && host == "*" {
|
||||
// User explicitly requested wildcard dst ip
|
||||
return filter.NetAny, nil
|
||||
} else {
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
if ip == nil || len(ip) != 4 {
|
||||
return filter.NetNone, fmt.Errorf("ports=%#v: invalid IPv4 address", host)
|
||||
}
|
||||
return filter.Net{
|
||||
IP: filter.NewIP(ip),
|
||||
Mask: filter.Netmask(defaultBits),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire format,
|
||||
// producing the most current filter.Matches format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
mm := make([]filter.Match, 0, len(pf))
|
||||
var erracc error
|
||||
|
||||
for _, r := range pf {
|
||||
m := filter.Match{}
|
||||
|
||||
for i, s := range r.SrcIPs {
|
||||
bits := 32
|
||||
if len(r.SrcBits) > i {
|
||||
bits = r.SrcBits[i]
|
||||
}
|
||||
net, err := parseIP(s, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Srcs = append(m.Srcs, net)
|
||||
}
|
||||
|
||||
for _, d := range r.DstPorts {
|
||||
bits := 32
|
||||
if d.Bits != nil {
|
||||
bits = *d.Bits
|
||||
}
|
||||
net, err := parseIP(d.IP, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Dsts = append(m.Dsts, filter.NetPortRange{
|
||||
Net: net,
|
||||
Ports: filter.PortRange{
|
||||
First: d.Ports.First,
|
||||
Last: d.Ports.Last,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
mm = append(mm, m)
|
||||
}
|
||||
|
||||
if erracc != nil {
|
||||
c.logf("parsePacketFilter: %s\n", erracc)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
|
||||
42
derp/derp.go
42
derp/derp.go
@@ -39,10 +39,14 @@ const (
|
||||
keepAlive = 60 * time.Second
|
||||
)
|
||||
|
||||
// ProtocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// protocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// * version 1 (zero on wire): consistent box headers, in use by employee dev nodes a bit
|
||||
// * version 2: received packets have src addrs in frameRecvPacket at beginning
|
||||
const ProtocolVersion = 2
|
||||
const protocolVersion = 2
|
||||
|
||||
const (
|
||||
protocolSrcAddrs = 2 // protocol version at which client expects src addresses
|
||||
)
|
||||
|
||||
// frameType is the one byte frame type at the beginning of the frame
|
||||
// header. The second field is a big-endian uint32 describing the
|
||||
@@ -104,31 +108,16 @@ var bin = binary.BigEndian
|
||||
func writeUint32(bw *bufio.Writer, v uint32) error {
|
||||
var b [4]byte
|
||||
bin.PutUint32(b[:], v)
|
||||
// Writing a byte at a time is a bit silly,
|
||||
// but it causes b not to escape,
|
||||
// which more than pays for the silliness.
|
||||
for _, c := range &b {
|
||||
err := bw.WriteByte(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
_, err := bw.Write(b[:])
|
||||
return err
|
||||
}
|
||||
|
||||
func readUint32(br *bufio.Reader) (uint32, error) {
|
||||
var b [4]byte
|
||||
// Reading a byte at a time is a bit silly,
|
||||
// but it causes b not to escape,
|
||||
// which more than pays for the silliness.
|
||||
for i := range &b {
|
||||
c, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
b[i] = c
|
||||
b := make([]byte, 4)
|
||||
if _, err := io.ReadFull(br, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return bin.Uint32(b[:]), nil
|
||||
return bin.Uint32(b), nil
|
||||
}
|
||||
|
||||
func readFrameTypeHeader(br *bufio.Reader, wantType frameType) (frameLen uint32, err error) {
|
||||
@@ -205,6 +194,13 @@ func writeFrame(bw *bufio.Writer, t frameType, b []byte) error {
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func minUint32(a, b uint32) uint32 {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@@ -21,13 +21,14 @@ import (
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
protoVersion int // min of server+client
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
@@ -48,8 +49,7 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
MeshKey string
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -58,12 +58,6 @@ type clientOpt struct {
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
|
||||
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
|
||||
// If key is the zero value, the returned ClientOpt is a no-op.
|
||||
func ServerPublicKey(key key.Public) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
@@ -85,16 +79,17 @@ func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
}
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
|
||||
}
|
||||
} else {
|
||||
c.serverKey = opt.ServerPub
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
|
||||
}
|
||||
if err := c.sendClientKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to send client key: %v", err)
|
||||
}
|
||||
info, err := c.recvServerInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server info: %v", err)
|
||||
}
|
||||
c.protoVersion = minInt(protocolVersion, info.Version)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -115,9 +110,12 @@ func (c *Client) recvServerKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
func (c *Client) recvServerInfo() (*serverInfo, error) {
|
||||
fl, err := readFrameTypeHeader(c.br, frameServerInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const maxLength = nonceLen + maxInfoLen
|
||||
fl := len(b)
|
||||
if fl < nonceLen {
|
||||
return nil, fmt.Errorf("short serverInfo frame")
|
||||
}
|
||||
@@ -126,27 +124,33 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
}
|
||||
// TODO: add a read-nonce-and-box helper
|
||||
var nonce [nonceLen]byte
|
||||
copy(nonce[:], b)
|
||||
msgbox := b[nonceLen:]
|
||||
if _, err := io.ReadFull(c.br, nonce[:]); err != nil {
|
||||
return nil, fmt.Errorf("nonce: %v", err)
|
||||
}
|
||||
msgLen := fl - nonceLen
|
||||
msgbox := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(c.br, msgbox); err != nil {
|
||||
return nil, fmt.Errorf("msgbox: %v", err)
|
||||
}
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %x", c.serverKey[:])
|
||||
return nil, fmt.Errorf("msgbox: cannot open len=%d with server key %x", msgLen, c.serverKey[:])
|
||||
}
|
||||
info := new(serverInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
return nil, fmt.Errorf("msg: %v", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
Version int // `json:"version,omitempty"`
|
||||
|
||||
// MeshKey optionally specifies a pre-shared key used by
|
||||
// trusted clients. It's required to subscribe to the
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
MeshKey string // `json:"meshKey,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -155,7 +159,7 @@ func (c *Client) sendClientKey() error {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
Version: protocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -314,11 +318,6 @@ type PeerPresentMessage key.Public
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
|
||||
// ServerInfoMessage is sent by the server upon first connect.
|
||||
type ServerInfoMessage struct{}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
@@ -365,7 +364,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
// If the frame fits in our bufio.Reader buffer, just use it.
|
||||
// In practice it's 4KB (from derphttp.Client's bufio.NewReader(httpConn)) and
|
||||
// in practive, WireGuard packets (and thus DERP frames) are under 1.5KB.
|
||||
// So this is the common path.
|
||||
// So This is the common path.
|
||||
if int(n) <= c.br.Size() {
|
||||
b, err = c.br.Peek(int(n))
|
||||
c.peeked = int(n)
|
||||
@@ -383,19 +382,6 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
switch t {
|
||||
default:
|
||||
continue
|
||||
case frameServerInfo:
|
||||
// Server sends this at start-up. Currently unused.
|
||||
// Just has a JSON message saying "version: 2",
|
||||
// but the protocol seems extensible enough as-is without
|
||||
// needing to wait an RTT to discover the version at startup.
|
||||
// We'd prefer to give the connection to the client (magicsock)
|
||||
// to start writing as soon as possible.
|
||||
_, err := c.parseServerInfo(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid server info frame: %v", err)
|
||||
}
|
||||
// TODO: add the results of parseServerInfo to ServerInfoMessage if we ever need it.
|
||||
return ServerInfoMessage{}, nil
|
||||
case frameKeepAlive:
|
||||
// TODO: eventually we'll have server->client pings that
|
||||
// require ack pongs.
|
||||
@@ -420,12 +406,16 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
|
||||
case frameRecvPacket:
|
||||
var rp ReceivedPacket
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
if c.protoVersion < protocolSrcAddrs {
|
||||
rp.Data = b[:n]
|
||||
} else {
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,14 @@ package derp
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -29,10 +24,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -41,29 +34,6 @@ import (
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
|
||||
|
||||
// verboseDropKeys is the set of destination public keys that should
|
||||
// verbosely log whenever DERP drops a packet.
|
||||
var verboseDropKeys = map[key.Public]bool{}
|
||||
|
||||
func init() {
|
||||
keys := os.Getenv("TS_DEBUG_VERBOSE_DROPS")
|
||||
if keys == "" {
|
||||
return
|
||||
}
|
||||
for _, keyStr := range strings.Split(keys, ",") {
|
||||
k, err := key.NewPublicFromHexMem(mem.S(keyStr))
|
||||
if err != nil {
|
||||
log.Printf("ignoring invalid debug key %q: %v", keyStr, err)
|
||||
} else {
|
||||
verboseDropKeys[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
const (
|
||||
perClientSendQueueDepth = 32 // packets buffered for sending
|
||||
writeTimeout = 2 * time.Second
|
||||
@@ -82,22 +52,16 @@ type Server struct {
|
||||
// before failing when writing to a client.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
privateKey key.Private
|
||||
publicKey key.Public
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
limitedLogf logger.Logf
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
privateKey key.Private
|
||||
publicKey key.Public
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
|
||||
// Counters:
|
||||
_ [pad32bit]byte
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ [pad32bit]byte
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedUnknown *expvar.Int // unknown dst pubkey
|
||||
@@ -172,8 +136,6 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
|
||||
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
|
||||
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
|
||||
clients: map[key.Public]*sclient{},
|
||||
clientsEver: map[key.Public]bool{},
|
||||
@@ -183,9 +145,6 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
watchers: map[*sclient]bool{},
|
||||
sentTo: map[key.Public]map[key.Public]int64{},
|
||||
}
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
|
||||
s.packetsDroppedUnknown = s.packetsDroppedReason.Get("unknown_dest")
|
||||
s.packetsDroppedFwdUnknown = s.packetsDroppedReason.Get("unknown_dest_on_fwd")
|
||||
s.packetsDroppedGone = s.packetsDroppedReason.Get("gone")
|
||||
@@ -277,50 +236,6 @@ func (s *Server) Accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
|
||||
}
|
||||
}
|
||||
|
||||
// initMetacert initialized s.metaCert with a self-signed x509 cert
|
||||
// encoding this server's public key and protocol version. cmd/derper
|
||||
// then sends this after the Let's Encrypt leaf + intermediate certs
|
||||
// after the ServerHello (encrypted in TLS 1.3, not that it matters
|
||||
// much).
|
||||
//
|
||||
// Then the client can save a round trip getting that and can start
|
||||
// speaking DERP right away. (We don't use ALPN because that's sent in
|
||||
// the clear and we're being paranoid to not look too weird to any
|
||||
// middleboxes, given that DERP is an ultimate fallback path). But
|
||||
// since the post-ServerHello certs are encrypted we can have the
|
||||
// client also use them as a signal to be able to start speaking DERP
|
||||
// right away, starting with its identity proof, encrypted to the
|
||||
// server's public key.
|
||||
//
|
||||
// This RTT optimization fails where there's a corp-mandated
|
||||
// TLS proxy with corp-mandated root certs on employee machines and
|
||||
// and TLS proxy cleans up unnecessary certs. In that case we just fall
|
||||
// back to the extra RTT.
|
||||
func (s *Server) initMetacert() {
|
||||
pub, priv, err := ed25519.GenerateKey(crand.Reader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(ProtocolVersion),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("derpkey%x", s.publicKey[:]),
|
||||
},
|
||||
// Windows requires NotAfter and NotBefore set:
|
||||
NotAfter: time.Now().Add(30 * 24 * time.Hour),
|
||||
NotBefore: time.Now().Add(-30 * 24 * time.Hour),
|
||||
}
|
||||
cert, err := x509.CreateCertificate(crand.Reader, tmpl, tmpl, pub, priv)
|
||||
if err != nil {
|
||||
log.Fatalf("CreateCertificate: %v", err)
|
||||
}
|
||||
s.metaCert = cert
|
||||
}
|
||||
|
||||
// MetaCert returns the server metadata cert that can be sent by the
|
||||
// TLS server to let the client skip a round trip during start-up.
|
||||
func (s *Server) MetaCert() []byte { return s.metaCert }
|
||||
|
||||
// registerClient notes that client c is now authenticated and ready for packets.
|
||||
// If c's public key was already connected with a different connection, the prior one is closed.
|
||||
func (s *Server) registerClient(c *sclient) {
|
||||
@@ -665,8 +580,10 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
|
||||
p := pkt{
|
||||
bs: contents,
|
||||
src: c.key,
|
||||
bs: contents,
|
||||
}
|
||||
if dst.info.Version >= protocolSrcAddrs {
|
||||
p.src = c.key
|
||||
}
|
||||
return c.sendPkt(dst, p)
|
||||
}
|
||||
@@ -699,12 +616,6 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
case <-dst.sendQueue:
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueHead.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("tail drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue head", dstKey)
|
||||
}
|
||||
@@ -716,12 +627,6 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// this case to keep reader unblocked.
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueTail.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("head drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue tail", dstKey)
|
||||
}
|
||||
@@ -763,7 +668,7 @@ func (s *Server) sendServerKey(bw *bufio.Writer) error {
|
||||
}
|
||||
|
||||
type serverInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
Version int // `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
@@ -771,7 +676,7 @@ func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(serverInfo{Version: ProtocolVersion})
|
||||
msg, err := json.Marshal(serverInfo{Version: protocolVersion})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -833,7 +738,7 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi
|
||||
if frameLen < keyLen {
|
||||
return zpub, nil, errors.New("short send packet frame")
|
||||
}
|
||||
if err := readPublicKey(br, &dstKey); err != nil {
|
||||
if _, err := io.ReadFull(br, dstKey[:]); err != nil {
|
||||
return zpub, nil, err
|
||||
}
|
||||
packetLen := frameLen - keyLen
|
||||
@@ -846,11 +751,6 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi
|
||||
}
|
||||
s.packetsRecv.Add(1)
|
||||
s.bytesRecv.Add(int64(len(contents)))
|
||||
if disco.LooksLikeDiscoWrapper(contents) {
|
||||
s.packetsRecvDisco.Add(1)
|
||||
} else {
|
||||
s.packetsRecvOther.Add(1)
|
||||
}
|
||||
return dstKey, contents, nil
|
||||
}
|
||||
|
||||
@@ -980,7 +880,11 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
|
||||
jitter := time.Duration(rand.Intn(5000)) * time.Millisecond
|
||||
jitterMs, err := crand.Int(crand.Reader, big.NewInt(5000))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
jitter := time.Duration(jitterMs.Int64()) * time.Millisecond
|
||||
keepAliveTick := time.NewTicker(keepAlive + jitter)
|
||||
defer keepAliveTick.Stop()
|
||||
|
||||
@@ -1136,8 +1040,7 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
return err
|
||||
}
|
||||
if withKey {
|
||||
err := writePublicKey(c.bw, &srcKey)
|
||||
if err != nil {
|
||||
if _, err = c.bw.Write(srcKey[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1278,7 +1181,6 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("bytes_sent", &s.bytesSent)
|
||||
m.Set("packets_dropped", &s.packetsDropped)
|
||||
m.Set("counter_packets_dropped_reason", &s.packetsDroppedReason)
|
||||
m.Set("counter_packets_received_kind", &s.packetsRecvByKind)
|
||||
m.Set("packets_sent", &s.packetsSent)
|
||||
m.Set("packets_received", &s.packetsRecv)
|
||||
m.Set("unknown_frames", &s.unknownFrames)
|
||||
@@ -1334,34 +1236,3 @@ func (s *Server) ConsistencyCheck() error {
|
||||
}
|
||||
return errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
|
||||
// readPublicKey reads key from br.
|
||||
// It is ~4x slower than io.ReadFull(br, key),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If io.ReadFull(br, key) does not cause key to escape, use that instead.
|
||||
func readPublicKey(br *bufio.Reader, key *key.Public) error {
|
||||
// Do io.ReadFull(br, key), but one byte at a time, to avoid allocation.
|
||||
for i := range key {
|
||||
b, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key[i] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writePublicKey writes key to bw.
|
||||
// It is ~3x slower than bw.Write(key[:]),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If bw.Write(key[:]) does not cause key to escape, use that instead.
|
||||
func writePublicKey(bw *bufio.Writer, key *key.Public) error {
|
||||
// Do bw.Write(key[:]), but one byte at a time to avoid allocation.
|
||||
for _, b := range key {
|
||||
err := bw.WriteByte(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,14 +8,10 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -35,22 +31,6 @@ func newPrivateKey(tb testing.TB) (k key.Private) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestClientInfoUnmarshal(t *testing.T) {
|
||||
for i, in := range []string{
|
||||
`{"Version":5,"MeshKey":"abc"}`,
|
||||
`{"version":5,"meshKey":"abc"}`,
|
||||
} {
|
||||
var got clientInfo
|
||||
if err := json.Unmarshal([]byte(in), &got); err != nil {
|
||||
t.Fatalf("[%d]: %v", i, err)
|
||||
}
|
||||
want := clientInfo{Version: 5, MeshKey: "abc"}
|
||||
if got != want {
|
||||
t.Errorf("[%d]: got %+v; want %+v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
@@ -99,8 +79,6 @@ func TestSendRecv(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("client %d: %v", i, err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
|
||||
clients = append(clients, c)
|
||||
recvChs = append(recvChs, make(chan []byte))
|
||||
t.Logf("Connected client %d.", i)
|
||||
@@ -140,7 +118,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if got := string(b); got != want {
|
||||
t.Errorf("client1.Recv=%q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
|
||||
}
|
||||
}
|
||||
@@ -246,7 +224,6 @@ func TestSendFreeze(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
return c, c2
|
||||
}
|
||||
|
||||
@@ -525,13 +502,7 @@ func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net
|
||||
func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.Private, logf logger.Logf) (*Client, error) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitConnect(t, c)
|
||||
return c, nil
|
||||
|
||||
return NewClient(priv, nc, brw, logf)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -542,7 +513,6 @@ func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitConnect(t, c)
|
||||
if err := c.WatchConnectionChanges(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -773,24 +743,6 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetaCert(t *testing.T) {
|
||||
priv := newPrivateKey(t)
|
||||
pub := priv.Public()
|
||||
s := NewServer(priv, t.Logf)
|
||||
|
||||
certBytes := s.MetaCert()
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(cert.SerialNumber) != fmt.Sprint(ProtocolVersion) {
|
||||
t.Errorf("serial = %v; want %v", cert.SerialNumber, ProtocolVersion)
|
||||
}
|
||||
if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%x", pub[:]); g != w {
|
||||
t.Errorf("CommonName = %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSendRecv(b *testing.B) {
|
||||
for _, size := range []int{10, 100, 1000, 10000} {
|
||||
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
|
||||
@@ -851,42 +803,3 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteUint32(b *testing.B) {
|
||||
w := bufio.NewWriter(ioutil.Discard)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writeUint32(w, 0x0ba3a)
|
||||
}
|
||||
}
|
||||
|
||||
type nopRead struct{}
|
||||
|
||||
func (r nopRead) Read(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var sinkU32 uint32
|
||||
|
||||
func BenchmarkReadUint32(b *testing.B) {
|
||||
r := bufio.NewReader(nopRead{})
|
||||
var err error
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkU32, err = readUint32(r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitConnect(t testing.TB, c *Client) {
|
||||
t.Helper()
|
||||
if m, err := c.Recv(); err != nil {
|
||||
t.Fatalf("client first Recv: %v", err)
|
||||
} else if v, ok := m.(ServerInfoMessage); !ok {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,27 +14,21 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -258,41 +252,14 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}
|
||||
}()
|
||||
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var serverPub key.Public // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
var serverProtoVersion int
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
if c.useHTTPS() {
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
httpConn = tlsConn
|
||||
|
||||
// Force a handshake now (instead of waiting for it to
|
||||
// be done implicitly on read/write) so we can check
|
||||
// the ConnectionState.
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// We expect to be using TLS 1.3 to our own servers, and only
|
||||
// starting at TLS 1.3 are the server's returned certificates
|
||||
// encrypted, so only look for and use our "meta cert" if we're
|
||||
// using TLS 1.3. If we're not using TLS 1.3, it might be a user
|
||||
// running cmd/derper themselves with a different configuration,
|
||||
// in which case we can avoid this fast-start optimization.
|
||||
// (If a corporate proxy is MITM'ing TLS 1.3 connections with
|
||||
// corp-mandated TLS root certs than all bets are off anyway.)
|
||||
// Note that we're not specifically concerned about TLS downgrade
|
||||
// attacks. TLS handles that fine:
|
||||
// https://blog.gypsyengineer.com/en/security/how-does-tls-1-3-protect-against-downgrade-attacks.html
|
||||
connState := tlsConn.ConnectionState()
|
||||
if connState.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(connState.PeerCertificates)
|
||||
}
|
||||
httpConn = c.tlsClient(tcpConn, node)
|
||||
} else {
|
||||
httpConn = tcpConn
|
||||
}
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(httpConn), bufio.NewWriter(httpConn))
|
||||
var derpClient *derp.Client
|
||||
|
||||
req, err := http.NewRequest("GET", c.urlString(node), nil)
|
||||
if err != nil {
|
||||
@@ -301,39 +268,24 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
req.Header.Set("Upgrade", "DERP")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
|
||||
if !serverPub.IsZero() && serverProtoVersion != 0 {
|
||||
// parseMetaCert found the server's public key (no TLS
|
||||
// middlebox was in the way), so skip the HTTP upgrade
|
||||
// exchange. See https://github.com/tailscale/tailscale/issues/693
|
||||
// for an overview. We still send the HTTP request
|
||||
// just to get routed into the server's HTTP Handler so it
|
||||
// can Hijack the request, but we signal with a special header
|
||||
// that we don't want to deal with its HTTP response.
|
||||
req.Header.Set(fastStartHeader, "1") // suppresses the server's HTTP response
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// No need to flush the HTTP request. the derp.Client's initial
|
||||
// client auth frame will flush it.
|
||||
} else {
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
|
||||
derpClient, err := derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -412,14 +364,6 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
}
|
||||
}
|
||||
if n := os.Getenv("SSLKEYLOGFILE"); n != "" {
|
||||
f, err := os.OpenFile(n, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("WARNING: writing to SSLKEYLOGFILE %v", n)
|
||||
tlsConf.KeyLogWriter = f
|
||||
}
|
||||
return tls.Client(nc, tlsConf)
|
||||
}
|
||||
|
||||
@@ -476,19 +420,6 @@ const dialNodeTimeout = 1500 * time.Millisecond
|
||||
// TODO(bradfitz): longer if no options remain perhaps? ... Or longer
|
||||
// overall but have dialRegion start overlapping races?
|
||||
func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, error) {
|
||||
// First see if we need to use an HTTP proxy.
|
||||
proxyReq := &http.Request{
|
||||
Method: "GET", // doesn't really matter
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: c.tlsServerName(n),
|
||||
Path: "/", // unused
|
||||
},
|
||||
}
|
||||
if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil {
|
||||
return c.dialNodeUsingProxy(ctx, n, proxyURL)
|
||||
}
|
||||
|
||||
type res struct {
|
||||
c net.Conn
|
||||
err error
|
||||
@@ -549,77 +480,6 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
}
|
||||
}
|
||||
|
||||
func firstStr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL.
|
||||
func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (proxyConn net.Conn, err error) {
|
||||
pu := proxyURL
|
||||
if pu.Scheme == "https" {
|
||||
var d tls.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "443")))
|
||||
} else {
|
||||
var d net.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "80")))
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && proxyConn != nil {
|
||||
// In a goroutine in case it's a *tls.Conn (that can block on Close)
|
||||
// TODO(bradfitz): track the underlying tcp.Conn and just close that instead.
|
||||
go proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
target := net.JoinHostPort(n.HostName, "443")
|
||||
|
||||
var authHeader string
|
||||
if v, err := tshttpproxy.GetAuthHeader(pu); err != nil {
|
||||
c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err)
|
||||
} else if v != "" {
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, pu.Hostname(), authHeader); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
br := bufio.NewReader(proxyConn)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, err)
|
||||
return nil, err
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, res.Status)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid response status from HTTP proxy %s on CONNECT to %s: %v", pu, target, res.Status)
|
||||
}
|
||||
return proxyConn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
|
||||
if err != nil {
|
||||
@@ -754,16 +614,3 @@ func (c *Client) closeForReconnect(brokenClient *derp.Client) {
|
||||
}
|
||||
|
||||
var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.Public, serverProtoVersion int) {
|
||||
for _, cert := range certs {
|
||||
if cn := cert.Subject.CommonName; strings.HasPrefix(cn, "derpkey") {
|
||||
var err error
|
||||
serverPub, err = key.NewPublicFromHexMem(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
return serverPub, int(cert.SerialNumber.Int64())
|
||||
}
|
||||
}
|
||||
}
|
||||
return key.Public{}, 0
|
||||
}
|
||||
|
||||
@@ -5,51 +5,33 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.Header.Get("Upgrade"); p != "WebSocket" && p != "DERP" {
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
w.Header().Set("Upgrade", "DERP")
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := h.Hijack()
|
||||
if err != nil {
|
||||
log.Printf("Hijack failed: %v", err)
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := s.PublicKey()
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %x\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey[:])
|
||||
}
|
||||
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,15 +19,22 @@ import (
|
||||
)
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := key.NewPrivate()
|
||||
|
||||
const numClients = 3
|
||||
var serverPrivateKey key.Private
|
||||
if _, err := crand.Read(serverPrivateKey[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var clientPrivateKeys []key.Private
|
||||
var clientKeys []key.Public
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := key.NewPrivate()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
var key key.Private
|
||||
if _, err := crand.Read(key[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPrivateKeys = append(clientPrivateKeys, key)
|
||||
}
|
||||
var clientKeys []key.Public
|
||||
for _, privKey := range clientPrivateKeys {
|
||||
clientKeys = append(clientKeys, privKey.Public())
|
||||
}
|
||||
|
||||
s := derp.NewServer(serverPrivateKey, t.Logf)
|
||||
@@ -73,7 +81,6 @@ func TestSendRecv(t *testing.T) {
|
||||
if err := c.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("client %d Connect: %v", i, err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
clients = append(clients, c)
|
||||
recvChs = append(recvChs, make(chan []byte))
|
||||
|
||||
@@ -88,11 +95,6 @@ func TestSendRecv(t *testing.T) {
|
||||
}
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
t.Logf("client%d: %v", i, err)
|
||||
break
|
||||
}
|
||||
@@ -116,7 +118,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if got := string(b); got != want {
|
||||
t.Errorf("client1.Recv=%q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
|
||||
}
|
||||
}
|
||||
@@ -144,13 +146,5 @@ func TestSendRecv(t *testing.T) {
|
||||
recv(2, string(msg2))
|
||||
recvNothing(0)
|
||||
recvNothing(1)
|
||||
}
|
||||
|
||||
func waitConnect(t testing.TB, c *Client) {
|
||||
t.Helper()
|
||||
if m, err := c.Recv(); err != nil {
|
||||
t.Fatalf("client first Recv: %v", err)
|
||||
} else if v, ok := m.(derp.ServerInfoMessage); !ok {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package derpmap contains information about Tailscale.com's production DERP nodes.
|
||||
//
|
||||
// This package is only used by the "tailscale netcheck" command for debugging.
|
||||
// In normal operation the Tailscale nodes get this sent to them from the control
|
||||
// server.
|
||||
//
|
||||
// TODO: remove this package and make "tailscale netcheck" get the
|
||||
// list from the control server too.
|
||||
package derpmap
|
||||
|
||||
import (
|
||||
@@ -28,10 +21,9 @@ func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode {
|
||||
}
|
||||
}
|
||||
|
||||
func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
func derpRegion(id int, code string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
region := &tailcfg.DERPRegion{
|
||||
RegionID: id,
|
||||
RegionName: name,
|
||||
RegionCode: code,
|
||||
Nodes: nodes,
|
||||
}
|
||||
@@ -53,36 +45,21 @@ func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.
|
||||
func Prod() *tailcfg.DERPMap {
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: derpRegion(1, "nyc", "New York City",
|
||||
1: derpRegion(1, "nyc",
|
||||
derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"),
|
||||
),
|
||||
2: derpRegion(2, "sfo", "San Francisco",
|
||||
2: derpRegion(2, "sfo",
|
||||
derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"),
|
||||
),
|
||||
3: derpRegion(3, "sin", "Singapore",
|
||||
3: derpRegion(3, "sin",
|
||||
derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"),
|
||||
),
|
||||
4: derpRegion(4, "fra", "Frankfurt",
|
||||
4: derpRegion(4, "fra",
|
||||
derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"),
|
||||
),
|
||||
5: derpRegion(5, "syd", "Sydney",
|
||||
5: derpRegion(5, "syd",
|
||||
derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"),
|
||||
),
|
||||
6: derpRegion(6, "blr", "Bangalore",
|
||||
derpNode("a", "68.183.90.120", "2400:6180:100:d0::982:d001"),
|
||||
),
|
||||
7: derpRegion(7, "tok", "Tokyo",
|
||||
derpNode("a", "167.179.89.145", "2401:c080:1000:467f:5400:2ff:feee:22aa"),
|
||||
),
|
||||
8: derpRegion(8, "lhr", "London",
|
||||
derpNode("a", "167.71.139.179", "2a03:b0c0:1:e0::3cc:e001"),
|
||||
),
|
||||
9: derpRegion(9, "dfw", "Dallas",
|
||||
derpNode("a", "207.148.3.137", "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"),
|
||||
),
|
||||
10: derpRegion(10, "sea", "Seattle",
|
||||
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
5
go.mod
5
go.mod
@@ -3,7 +3,6 @@ module tailscale.com
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
@@ -22,7 +21,7 @@ require (
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
|
||||
@@ -30,7 +29,7 @@ require (
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
|
||||
8
go.sum
8
go.sum
@@ -9,8 +9,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=
|
||||
@@ -90,8 +88,6 @@ github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f h1:uFj5bslHs
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a h1:dQEgNpoOJf+8MswlvXJicb8ZDQqZAGe8f/WfzbDMvtE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4 h1:UiTXdZChEWxxci7bx+jS9OyHQx2IA8zmMWQqp5wfP7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
@@ -117,7 +113,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -138,15 +133,12 @@ golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
||||
@@ -62,7 +62,6 @@ type Notify struct {
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
@@ -144,9 +143,6 @@ type Backend interface {
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(*Prefs)
|
||||
// SetWantRunning is like SetPrefs but sets only the
|
||||
// WantRunning field.
|
||||
SetWantRunning(wantRunning bool)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
@@ -160,8 +156,4 @@ type Backend interface {
|
||||
// make sure they react properly with keys that are going to
|
||||
// expire.
|
||||
FakeExpireAfter(x time.Duration)
|
||||
// Ping attempts to start connecting to the given IP and sends a Notify
|
||||
// with its PingResult. If the host is down, there might never
|
||||
// be a PingResult sent. The cmd/tailscale CLI client adds a timeout.
|
||||
Ping(ip string)
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FakeBackend) SetWantRunning(v bool) {
|
||||
b.SetPrefs(&Prefs{WantRunning: v})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestEngineStatus() {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
@@ -94,7 +90,3 @@ func (b *FakeBackend) RequestStatus() {
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
|
||||
}
|
||||
|
||||
@@ -27,10 +27,8 @@ import (
|
||||
type Status struct {
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
}
|
||||
|
||||
func (s *Status) Peers() []key.Public {
|
||||
@@ -45,7 +43,6 @@ func (s *Status) Peers() []key.Public {
|
||||
type PeerStatus struct {
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
DNSName string
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
@@ -91,12 +88,6 @@ type StatusBuilder struct {
|
||||
st Status
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetBackendState(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.BackendState = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@@ -104,13 +95,6 @@ func (sb *StatusBuilder) Status() *Status {
|
||||
return &sb.st
|
||||
}
|
||||
|
||||
// SetSelfStatus sets the status of the local machine.
|
||||
func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.Self = ss
|
||||
}
|
||||
|
||||
// AddUser adds a user profile to the status.
|
||||
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
sb.mu.Lock()
|
||||
@@ -167,9 +151,6 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if v := st.HostName; v != "" {
|
||||
e.HostName = v
|
||||
}
|
||||
if v := st.DNSName; v != "" {
|
||||
e.DNSName = v
|
||||
}
|
||||
if v := st.Relay; v != "" {
|
||||
e.Relay = v
|
||||
}
|
||||
@@ -341,21 +322,3 @@ func osEmoji(os string) string {
|
||||
}
|
||||
return "👽"
|
||||
}
|
||||
|
||||
// PingResult contains response information for the "tailscale ping" subcommand,
|
||||
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
|
||||
type PingResult struct {
|
||||
IP string // ping destination
|
||||
NodeIP string // Tailscale IP of node handling IP (different for subnet routers)
|
||||
NodeName string // DNS name base or (possibly not unique) hostname
|
||||
|
||||
Err string
|
||||
LatencySeconds float64
|
||||
|
||||
Endpoint string // ip:port if direct UDP was used
|
||||
|
||||
DERPRegionID int // non-zero if DERP was used
|
||||
DERPRegionCode string // three-letter airport/region code if DERP was used
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
44
ipn/local.go
44
ipn/local.go
@@ -19,7 +19,6 @@ import (
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -110,23 +109,11 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
|
||||
state: NoState,
|
||||
portpoll: portpoll,
|
||||
}
|
||||
e.SetLinkChangeCallback(b.linkChange)
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
// TODO(bradfitz): on a major link change, ask controlclient
|
||||
// whether its host (e.g. login.tailscale.com) is reachable.
|
||||
// If not, down the world and poll for a bit. Windows' WinHTTP
|
||||
// service might be unable to resolve its WPAD PAC URL if we
|
||||
// have DNS/routes configured. So we need to remove that DNS
|
||||
// and those routes to let it figure out its proxy
|
||||
// settings. Once it's back up and happy, then we can resume
|
||||
// and our connection to the control server would work again.
|
||||
}
|
||||
|
||||
// Shutdown halts the backend and all its sub-components. The backend
|
||||
// can no longer be used after Shutdown returns.
|
||||
func (b *LocalBackend) Shutdown() {
|
||||
@@ -157,8 +144,6 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
sb.SetBackendState(b.state.String())
|
||||
|
||||
// TODO: hostinfo, and its networkinfo
|
||||
// TODO: EngineStatus copy (and deprecate it?)
|
||||
if b.netMap != nil {
|
||||
@@ -179,7 +164,6 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
UserID: p.User,
|
||||
TailAddr: tailAddr,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
@@ -566,7 +550,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
}
|
||||
set(netMap.Name, netMap.Addresses)
|
||||
|
||||
dnsMap := tsdns.NewMap(nameToIP, domainsForProxying(netMap))
|
||||
dnsMap := tsdns.NewMap(nameToIP)
|
||||
// map diff will be logged in tsdns.Resolver.SetMap.
|
||||
b.e.SetDNSMap(dnsMap)
|
||||
}
|
||||
@@ -761,17 +745,6 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.send(Notify{NetMap: b.netMap})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Ping(ipStr string) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
b.logf("ignoring Ping request to invalid IP %q", ipStr)
|
||||
return
|
||||
}
|
||||
b.e.Ping(ip, func(pr *ipnstate.PingResult) {
|
||||
b.send(Notify{PingResult: pr})
|
||||
})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
|
||||
var (
|
||||
peerStats []string
|
||||
@@ -792,9 +765,7 @@ func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
|
||||
ret.WBytes += p.TxBytes
|
||||
}
|
||||
if len(peerStats) > 0 {
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.keyLogf("peer keys: %s", strings.Join(peerKeys, " "))
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.logf("v%v peers: %v", version.LONG, strings.Join(peerStats, " "))
|
||||
}
|
||||
return ret
|
||||
@@ -812,18 +783,6 @@ func (b *LocalBackend) shieldsAreUp() bool {
|
||||
return b.prefs.ShieldsUp
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetWantRunning(wantRunning bool) {
|
||||
b.mu.Lock()
|
||||
new := b.prefs.Clone()
|
||||
b.mu.Unlock()
|
||||
if new.WantRunning == wantRunning {
|
||||
return
|
||||
}
|
||||
new.WantRunning = wantRunning
|
||||
b.logf("SetWantRunning: %v", wantRunning)
|
||||
b.SetPrefs(new)
|
||||
}
|
||||
|
||||
// SetPrefs saves new user preferences and propagates them throughout
|
||||
// the system. Implements Backend.
|
||||
func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
@@ -857,7 +816,6 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.logf("SetPrefs: %v", new.Pretty())
|
||||
|
||||
if old.ShieldsUp != new.ShieldsUp || hostInfoChanged {
|
||||
|
||||
@@ -1,107 +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.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
// TestLocalLogLines tests to make sure that the log lines required for log parsing are
|
||||
// being logged by the expected functions. Update these tests if moving log lines between
|
||||
// functions.
|
||||
func TestLocalLogLines(t *testing.T) {
|
||||
logListen := tstest.ListenFor(t.Logf, []string{
|
||||
"SetPrefs: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
})
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
for i := 0; i < len(ret); i++ {
|
||||
ret[i] = hex
|
||||
}
|
||||
return ret
|
||||
}
|
||||
idA := logid(0xaa)
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &MemoryStore{
|
||||
cache: make(map[StateKey][]byte),
|
||||
}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lb, err := NewLocalBackend(logListen.Logf, idA.String(), store, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = NewPrefs()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
|
||||
// testing infrastructure
|
||||
type linesTest struct {
|
||||
name string
|
||||
want []string
|
||||
}
|
||||
|
||||
tests := []linesTest{
|
||||
{
|
||||
name: "after prefs",
|
||||
want: []string{
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "after peers",
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
testLogs := func(want linesTest) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
if linesLeft := logListen.Check(); len(linesLeft) != len(want.want) {
|
||||
t.Errorf("got %v, expected %v", linesLeft, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log prefs line
|
||||
persist := &controlclient.Persist{}
|
||||
prefs := NewPrefs()
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
t.Run(tests[0].name, testLogs(tests[0]))
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
}},
|
||||
LocalAddrs: []string{"idk an address"},
|
||||
}
|
||||
lb.parseWgStatus(status)
|
||||
|
||||
t.Run(tests[1].name, testLogs(tests[1]))
|
||||
}
|
||||
@@ -33,10 +33,6 @@ type FakeExpireAfterArgs struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type PingArgs struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
// Command is a command message that is JSON encoded and sent by a
|
||||
// frontend to a backend.
|
||||
type Command struct {
|
||||
@@ -57,11 +53,9 @@ type Command struct {
|
||||
Login *oauth2.Token
|
||||
Logout *NoArgs
|
||||
SetPrefs *SetPrefsArgs
|
||||
SetWantRunning *bool
|
||||
RequestEngineStatus *NoArgs
|
||||
RequestStatus *NoArgs
|
||||
FakeExpireAfter *FakeExpireAfterArgs
|
||||
Ping *PingArgs
|
||||
}
|
||||
|
||||
type BackendServer struct {
|
||||
@@ -145,9 +139,6 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.SetPrefs; c != nil {
|
||||
bs.b.SetPrefs(c.New)
|
||||
return nil
|
||||
} else if c := cmd.SetWantRunning; c != nil {
|
||||
bs.b.SetWantRunning(*c)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
@@ -157,9 +148,6 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
@@ -266,14 +254,6 @@ func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
||||
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Ping(ip string) {
|
||||
bc.send(Command{Ping: &PingArgs{IP: ip}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) SetWantRunning(v bool) {
|
||||
bc.send(Command{SetWantRunning: &v})
|
||||
}
|
||||
|
||||
// MaxMessageSize is the maximum message size, in bytes.
|
||||
const MaxMessageSize = 10 << 20
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ import (
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
@@ -105,23 +103,12 @@ func (l logWriter) Write(buf []byte) (int, error) {
|
||||
// logsDir returns the directory to use for log configuration and
|
||||
// buffer storage.
|
||||
func logsDir(logf logger.Logf) string {
|
||||
// STATE_DIRECTORY is set by systemd 240+ but we support older
|
||||
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
|
||||
systemdStateDir := os.Getenv("STATE_DIRECTORY")
|
||||
if systemdStateDir != "" {
|
||||
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
|
||||
return systemdStateDir
|
||||
}
|
||||
|
||||
// Default to e.g. /var/lib/tailscale or /var/db/tailscale on Unix.
|
||||
if d := paths.DefaultTailscaledStateFile(); d != "" {
|
||||
d = filepath.Dir(d) // directory of e.g. "/var/lib/tailscale/tailscaled.state"
|
||||
if err := os.MkdirAll(d, 0700); err == nil {
|
||||
logf("logpolicy: using system state directory %q", d)
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err == nil {
|
||||
d := filepath.Join(cacheDir, "Tailscale")
|
||||
@@ -432,9 +419,6 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
// Start with a copy of http.DefaultTransport and tweak it a bit.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
|
||||
// We do our own zstd compression on uploads, and responses never contain any payload,
|
||||
// so don't send "Accept-Encoding: gzip" to save a few bytes on the wire, since there
|
||||
// will never be any body to decompress:
|
||||
|
||||
@@ -8,19 +8,13 @@ package interfaces
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
// LoginEndpointForProxyDetermination is the URL used for testing
|
||||
// which HTTP proxy the system should use.
|
||||
var LoginEndpointForProxyDetermination = "https://login.tailscale.com/"
|
||||
|
||||
// Tailscale returns the current machine's Tailscale interface, if any.
|
||||
// If none is found, all zero values are returned.
|
||||
// A non-nil error is only returned on a problem listing the system interfaces.
|
||||
@@ -49,8 +43,7 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
||||
// maybeTailscaleInterfaceName reports whether s is an interface
|
||||
// name that might be used by Tailscale.
|
||||
func maybeTailscaleInterfaceName(s string) bool {
|
||||
return s == "Tailscale" ||
|
||||
strings.HasPrefix(s, "wg") ||
|
||||
return strings.HasPrefix(s, "wg") ||
|
||||
strings.HasPrefix(s, "ts") ||
|
||||
strings.HasPrefix(s, "tailscale") ||
|
||||
strings.HasPrefix(s, "utun")
|
||||
@@ -170,13 +163,6 @@ type State struct {
|
||||
// considered "expensive", which currently means LTE/etc
|
||||
// instead of Wifi. This field is not populated by GetState.
|
||||
IsExpensive bool
|
||||
|
||||
// DefaultRouteInterface is the interface name for the machine's default route.
|
||||
// It is not yet populated on all OSes.
|
||||
DefaultRouteInterface string
|
||||
|
||||
// HTTPProxy is the HTTP proxy to use.
|
||||
HTTPProxy string
|
||||
}
|
||||
|
||||
func (s *State) Equal(s2 *State) bool {
|
||||
@@ -189,8 +175,7 @@ func (s *State) Equal(s2 *State) bool {
|
||||
// /^tailscale/)
|
||||
func (s *State) RemoveTailscaleInterfaces() {
|
||||
for name := range s.InterfaceIPs {
|
||||
if name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
if strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
delete(s.InterfaceIPs, name)
|
||||
delete(s.InterfaceUp, name)
|
||||
}
|
||||
@@ -213,16 +198,6 @@ func GetState() (*State, error) {
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.DefaultRouteInterface, _ = DefaultRouteInterface()
|
||||
|
||||
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
|
||||
s.HTTPProxy = u.String()
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ import "C"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
@@ -115,6 +117,7 @@ func init() {
|
||||
|
||||
func likelyHomeRouterIPDarwinSyscall() (ret netaddr.IP, ok bool) {
|
||||
ip := C.privateGatewayIP()
|
||||
fmt.Fprintln(os.Stderr, "likelyHomeRouterIPDarwinSyscall", ip)
|
||||
if ip < 255 {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
|
||||
@@ -1,15 +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.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package interfaces
|
||||
|
||||
import "errors"
|
||||
|
||||
var errTODO = errors.New("TODO")
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
return "TODO", errTODO
|
||||
}
|
||||
@@ -171,15 +171,6 @@ type STUNConn interface {
|
||||
ReadFrom([]byte) (int, net.Addr, error)
|
||||
}
|
||||
|
||||
func (c *Client) enoughRegions() int {
|
||||
if c.Verbose {
|
||||
// Abuse verbose a bit here so netcheck can show all region latencies
|
||||
// in verbose mode.
|
||||
return 100
|
||||
}
|
||||
return 3
|
||||
}
|
||||
|
||||
func (c *Client) logf(format string, a ...interface{}) {
|
||||
if c.Logf != nil {
|
||||
c.Logf(format, a...)
|
||||
@@ -624,13 +615,12 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
|
||||
ret.UDP = true
|
||||
updateLatency(ret.RegionLatency, node.RegionID, d)
|
||||
|
||||
// Once we've heard from enough regions (3), start a timer to
|
||||
// give up on the other ones. The timer's duration is a
|
||||
// function of whether this is our initial full probe or an
|
||||
// incremental one. For incremental ones, wait for the
|
||||
// duration of the slowest region. For initial ones, double
|
||||
// that.
|
||||
if len(ret.RegionLatency) == rs.c.enoughRegions() {
|
||||
// Once we've heard from 3 regions, start a timer to give up
|
||||
// on the other ones. The timer's duration is a function of
|
||||
// whether this is our initial full probe or an incremental
|
||||
// one. For incremental ones, wait for the duration of the
|
||||
// slowest region. For initial ones, double that.
|
||||
if len(ret.RegionLatency) == 3 {
|
||||
timeout := maxDurationValue(ret.RegionLatency)
|
||||
if !rs.incremental {
|
||||
timeout *= 2
|
||||
|
||||
@@ -137,15 +137,15 @@ func foreachAttr(b []byte, fn func(attrType uint16, a []byte) error) error {
|
||||
}
|
||||
attrType := binary.BigEndian.Uint16(b[:2])
|
||||
attrLen := int(binary.BigEndian.Uint16(b[2:4]))
|
||||
attrLenWithPad := (attrLen + 3) &^ 3
|
||||
attrLenPad := attrLen % 4
|
||||
b = b[4:]
|
||||
if attrLenWithPad > len(b) {
|
||||
if attrLen+attrLenPad > len(b) {
|
||||
return ErrMalformedAttrs
|
||||
}
|
||||
if err := fn(attrType, b[:attrLen]); err != nil {
|
||||
return err
|
||||
}
|
||||
b = b[attrLenWithPad:]
|
||||
b = b[attrLen+attrLenPad:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,41 +140,6 @@ var responseTests = []struct {
|
||||
wantAddr: net.ParseIP("2602:d1:b4cf:c100:38b2:31ff:feef:96f6"),
|
||||
wantPort: 37070,
|
||||
},
|
||||
|
||||
// Testing STUN attribute padding rules using STUN software attribute
|
||||
// with values of 1 & 3 length respectively before the XorMappedAddress attribute
|
||||
{
|
||||
name: "software-a",
|
||||
data: []byte{
|
||||
0x01, 0x01, 0x00, 0x14, 0x21, 0x12, 0xa4, 0x42,
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e, 0x80, 0x22, 0x00, 0x01,
|
||||
0x61, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x08,
|
||||
0x00, 0x01, 0xce, 0x66, 0x5e, 0x12, 0xa4, 0x43,
|
||||
},
|
||||
wantTID: []byte{
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e,
|
||||
},
|
||||
wantAddr: []byte{127, 0, 0, 1},
|
||||
wantPort: 61300,
|
||||
},
|
||||
{
|
||||
name: "software-abc",
|
||||
data: []byte{
|
||||
0x01, 0x01, 0x00, 0x14, 0x21, 0x12, 0xa4, 0x42,
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e, 0x80, 0x22, 0x00, 0x03,
|
||||
0x61, 0x62, 0x63, 0x00, 0x00, 0x20, 0x00, 0x08,
|
||||
0x00, 0x01, 0xce, 0x66, 0x5e, 0x12, 0xa4, 0x43,
|
||||
},
|
||||
wantTID: []byte{
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e,
|
||||
},
|
||||
wantAddr: []byte{127, 0, 0, 1},
|
||||
wantPort: 61300,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseResponse(t *testing.T) {
|
||||
|
||||
@@ -1,60 +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.
|
||||
|
||||
// Package tshttpproxy contains Tailscale additions to httpproxy not available
|
||||
// in golang.org/x/net/http/httpproxy. Notably, it aims to support Windows better.
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// sysProxyFromEnv, if non-nil, specifies a platform-specific ProxyFromEnvironment
|
||||
// func to use if http.ProxyFromEnvironment doesn't return a proxy.
|
||||
// For example, WPAD PAC files on Windows.
|
||||
var sysProxyFromEnv func(*http.Request) (*url.URL, error)
|
||||
|
||||
func ProxyFromEnvironment(req *http.Request) (*url.URL, error) {
|
||||
u, err := http.ProxyFromEnvironment(req)
|
||||
if u != nil && err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
if sysProxyFromEnv != nil {
|
||||
u, err := sysProxyFromEnv(req)
|
||||
if u != nil && err == nil {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sysAuthHeader func(*url.URL) (string, error)
|
||||
|
||||
// GetAuthHeader returns the Authorization header value to send to proxy u.
|
||||
func GetAuthHeader(u *url.URL) (string, error) {
|
||||
if fake := os.Getenv("TS_DEBUG_FAKE_PROXY_AUTH"); fake != "" {
|
||||
return fake, nil
|
||||
}
|
||||
if sysAuthHeader != nil {
|
||||
return sysAuthHeader(u)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var condSetTransportGetProxyConnectHeader func(*http.Transport)
|
||||
|
||||
// SetTarnsportGetProxyConnectHeader sets the provided Transport's
|
||||
// GetProxyConnectHeader field, if the current build of Go supports
|
||||
// it.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/41048.
|
||||
func SetTransportGetProxyConnectHeader(tr *http.Transport) {
|
||||
if f := condSetTransportGetProxyConnectHeader; f != nil {
|
||||
f(tr)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +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.
|
||||
|
||||
// +build tailscale_go
|
||||
|
||||
// We want to use https://github.com/golang/go/issues/41048 but it's only in the
|
||||
// Tailscale Go tree for now. Hence the build tag above.
|
||||
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const proxyAuthHeader = "Proxy-Authorization"
|
||||
|
||||
func init() {
|
||||
condSetTransportGetProxyConnectHeader = func(tr *http.Transport) {
|
||||
tr.GetProxyConnectHeader = func(ctx context.Context, proxyURL *url.URL, target string) (http.Header, error) {
|
||||
v, err := GetAuthHeader(proxyURL)
|
||||
if err != nil {
|
||||
log.Printf("failed to get proxy Auth header for %v; ignoring: %v", proxyURL, err)
|
||||
return nil, nil
|
||||
}
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return http.Header{proxyAuthHeader: []string{v}}, nil
|
||||
}
|
||||
tr.OnProxyConnectResponse = func(ctx context.Context, proxyURL *url.URL, connectReq *http.Request, res *http.Response) error {
|
||||
auth := connectReq.Header.Get(proxyAuthHeader)
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy: CONNECT response from %v for target %q (auth %q): %v", proxyURL, connectReq.Host, auth, res.Status)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,213 +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.
|
||||
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/alexbrainman/sspi/negotiate"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
winHTTP = windows.NewLazySystemDLL("winhttp.dll")
|
||||
httpOpenProc = winHTTP.NewProc("WinHttpOpen")
|
||||
closeHandleProc = winHTTP.NewProc("WinHttpCloseHandle")
|
||||
getProxyForUrlProc = winHTTP.NewProc("WinHttpGetProxyForUrl")
|
||||
)
|
||||
|
||||
func init() {
|
||||
sysProxyFromEnv = proxyFromWinHTTPOrCache
|
||||
sysAuthHeader = sysAuthHeaderWindows
|
||||
}
|
||||
|
||||
var cachedProxy struct {
|
||||
sync.Mutex
|
||||
val *url.URL
|
||||
}
|
||||
|
||||
func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
|
||||
if req.URL == nil {
|
||||
return nil, nil
|
||||
}
|
||||
urlStr := req.URL.String()
|
||||
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
proxy *url.URL
|
||||
err error
|
||||
}
|
||||
resc := make(chan result, 1)
|
||||
go func() {
|
||||
proxy, err := proxyFromWinHTTP(ctx, urlStr)
|
||||
resc <- result{proxy, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-resc:
|
||||
err := res.err
|
||||
if err == nil {
|
||||
cachedProxy.Lock()
|
||||
defer cachedProxy.Unlock()
|
||||
if was, now := fmt.Sprint(cachedProxy.val), fmt.Sprint(res.proxy); was != now {
|
||||
log.Printf("tshttpproxy: winhttp: updating cached proxy setting from %v to %v", was, now)
|
||||
}
|
||||
cachedProxy.val = res.proxy
|
||||
return res.proxy, nil
|
||||
}
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
|
||||
const ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
|
||||
return nil, nil
|
||||
}
|
||||
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
cachedProxy.Lock()
|
||||
defer cachedProxy.Unlock()
|
||||
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
|
||||
return cachedProxy.val, nil
|
||||
}
|
||||
}
|
||||
|
||||
func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
|
||||
whi, err := winHTTPOpen()
|
||||
if err != nil {
|
||||
log.Printf("winhttp: Open: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer whi.Close()
|
||||
|
||||
t0 := time.Now()
|
||||
v, err := whi.GetProxyForURL(urlStr)
|
||||
td := time.Since(t0).Round(time.Millisecond)
|
||||
if err := ctx.Err(); err != nil {
|
||||
log.Printf("tshttpproxy: winhttp: context canceled, ignoring GetProxyForURL(%q) after %v", urlStr, td)
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
// Discard all but first proxy value for now.
|
||||
if i := strings.Index(v, ";"); i != -1 {
|
||||
v = v[:i]
|
||||
}
|
||||
if !strings.HasPrefix(v, "https://") {
|
||||
v = "http://" + v
|
||||
}
|
||||
return url.Parse(v)
|
||||
}
|
||||
|
||||
var userAgent = windows.StringToUTF16Ptr("Tailscale")
|
||||
|
||||
const (
|
||||
winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4
|
||||
winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG = 0x00000100
|
||||
winHTTP_AUTOPROXY_AUTO_DETECT = 1
|
||||
winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
|
||||
winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
|
||||
)
|
||||
|
||||
func winHTTPOpen() (winHTTPInternet, error) {
|
||||
if err := httpOpenProc.Find(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r, _, err := httpOpenProc.Call(
|
||||
uintptr(unsafe.Pointer(userAgent)),
|
||||
winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
0, /* WINHTTP_NO_PROXY_NAME */
|
||||
0, /* WINHTTP_NO_PROXY_BYPASS */
|
||||
0)
|
||||
if r == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return winHTTPInternet(r), nil
|
||||
}
|
||||
|
||||
type winHTTPInternet windows.Handle
|
||||
|
||||
func (hi winHTTPInternet) Close() error {
|
||||
if err := closeHandleProc.Find(); err != nil {
|
||||
return err
|
||||
}
|
||||
r, _, err := closeHandleProc.Call(uintptr(hi))
|
||||
if r == 1 {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WINHTTP_AUTOPROXY_OPTIONS
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_autoproxy_options
|
||||
type autoProxyOptions struct {
|
||||
DwFlags uint32
|
||||
DwAutoDetectFlags uint32
|
||||
AutoConfigUrl *uint16
|
||||
_ uintptr
|
||||
_ uint32
|
||||
FAutoLogonIfChallenged bool
|
||||
}
|
||||
|
||||
// WINHTTP_PROXY_INFO
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_proxy_info
|
||||
type winHTTPProxyInfo struct {
|
||||
AccessType uint16
|
||||
Proxy *uint16
|
||||
ProxyBypass *uint16
|
||||
}
|
||||
|
||||
var proxyForURLOpts = &autoProxyOptions{
|
||||
DwFlags: winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG | winHTTP_AUTOPROXY_AUTO_DETECT,
|
||||
DwAutoDetectFlags: winHTTP_AUTO_DETECT_TYPE_DHCP, // | winHTTP_AUTO_DETECT_TYPE_DNS_A,
|
||||
}
|
||||
|
||||
func (hi winHTTPInternet) GetProxyForURL(urlStr string) (string, error) {
|
||||
if err := getProxyForUrlProc.Find(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out winHTTPProxyInfo
|
||||
r, _, err := getProxyForUrlProc.Call(
|
||||
uintptr(hi),
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(urlStr))),
|
||||
uintptr(unsafe.Pointer(proxyForURLOpts)),
|
||||
uintptr(unsafe.Pointer(&out)))
|
||||
if r == 1 {
|
||||
return windows.UTF16PtrToString(out.Proxy), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func sysAuthHeaderWindows(u *url.URL) (string, error) {
|
||||
spn := "HTTP/" + u.Hostname()
|
||||
creds, err := negotiate.AcquireCurrentUserCredentials()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("negotiate.AcquireCurrentUserCredentials: %w", err)
|
||||
}
|
||||
defer creds.Release()
|
||||
|
||||
secCtx, token, err := negotiate.NewClientContext(creds, spn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("negotiate.NewClientContext: %w", err)
|
||||
}
|
||||
defer secCtx.Release()
|
||||
|
||||
return "Negotiate " + base64.StdEncoding.EncodeToString(token), nil
|
||||
}
|
||||
@@ -41,10 +41,6 @@ func parsePort(s string) int {
|
||||
return int(port)
|
||||
}
|
||||
|
||||
func isLoopbackAddr(s string) bool {
|
||||
return strings.HasPrefix(s, "127.0.0.1:") || strings.HasPrefix(s, "127.0.0.1.")
|
||||
}
|
||||
|
||||
type nothing struct{}
|
||||
|
||||
// Lowest common denominator parser for "netstat -na" format.
|
||||
@@ -78,7 +74,7 @@ func parsePortsNetstat(output string) List {
|
||||
// not interested in non-listener sockets
|
||||
continue
|
||||
}
|
||||
if isLoopbackAddr(laddr) {
|
||||
if strings.HasPrefix(laddr, "127.0.0.1:") || strings.HasPrefix(laddr, "127.0.0.1.") {
|
||||
// not interested in loopback-bound listeners
|
||||
continue
|
||||
}
|
||||
@@ -89,7 +85,7 @@ func parsePortsNetstat(output string) List {
|
||||
proto = "udp"
|
||||
laddr = cols[len(cols)-2]
|
||||
raddr = cols[len(cols)-1]
|
||||
if isLoopbackAddr(laddr) {
|
||||
if strings.HasPrefix(laddr, "127.0.0.1:") || strings.HasPrefix(laddr, "127.0.0.1.") {
|
||||
// not interested in loopback-bound listeners
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -95,12 +95,9 @@ func addProcesses(pl []Port) ([]Port, error) {
|
||||
if port > 0 {
|
||||
pp := ProtoPort{proto, uint16(port)}
|
||||
p := m[pp]
|
||||
switch {
|
||||
case p != nil:
|
||||
if p != nil {
|
||||
p.Process = cmd
|
||||
case isLoopbackAddr(val):
|
||||
// ignore
|
||||
default:
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "weird: missing %v\n", pp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@ type DERPRegion struct {
|
||||
// "fra", etc.
|
||||
RegionCode string
|
||||
|
||||
// RegionName is a long English name for the region: "New York City",
|
||||
// "San Francisco", "Singapore", "Frankfurt", etc.
|
||||
RegionName string
|
||||
|
||||
// Nodes are the DERP nodes running in this region, in
|
||||
// priority order for the current client. Client TLS
|
||||
// connections should ideally only go to the first entry
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig -output=tailcfg_clone.go
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo -output=tailcfg_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -496,18 +496,10 @@ var FilterAllowAll = []FilterRule{
|
||||
|
||||
// DNSConfig is the DNS configuration.
|
||||
type DNSConfig struct {
|
||||
// Nameservers are the IP addresses of the nameservers to use.
|
||||
Nameservers []netaddr.IP `json:",omitempty"`
|
||||
// Domains are the search domains to use.
|
||||
Domains []string `json:",omitempty"`
|
||||
// PerDomain indicates whether it is preferred to use Nameservers
|
||||
// only for DNS queries for subdomains of Domains.
|
||||
// Some OSes and OS configurations don't support per-domain DNS configuration,
|
||||
// in which case Nameservers applies to all DNS requests regardless of PerDomain's value.
|
||||
PerDomain bool
|
||||
// Proxied indicates whether DNS requests are proxied through a tsdns.Resolver.
|
||||
// This enables Magic DNS. It is togglable independently of PerDomain.
|
||||
Proxied bool
|
||||
Domains []string `json:",omitempty"`
|
||||
PerDomain bool
|
||||
Proxied bool
|
||||
}
|
||||
|
||||
type MapResponse struct {
|
||||
@@ -571,17 +563,6 @@ type Debug struct {
|
||||
// always do its background STUN queries (see magicsock's
|
||||
// periodicReSTUN), regardless of inactivity.
|
||||
ForceBackgroundSTUN bool `json:",omitempty"`
|
||||
|
||||
// DERPRoute controls whether the DERP reverse path
|
||||
// optimization (see Issue 150) should be enabled or
|
||||
// disabled. The environment variable in magicsock is the
|
||||
// highest priority (if set), then this (if set), then the
|
||||
// binary default value.
|
||||
DERPRoute opt.Bool `json:",omitempty"`
|
||||
|
||||
// TrimWGConfig controls whether Tailscale does lazy, on-demand
|
||||
// wireguard configuration of peers.
|
||||
TrimWGConfig opt.Bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
||||
@@ -647,7 +628,6 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
eqCIDRs(n.Addresses, n2.Addresses) &&
|
||||
eqCIDRs(n.AllowedIPs, n2.AllowedIPs) &&
|
||||
eqStrings(n.Endpoints, n2.Endpoints) &&
|
||||
n.DERP == n2.DERP &&
|
||||
n.Hostinfo.Equal(&n2.Hostinfo) &&
|
||||
n.Created.Equal(n2.Created) &&
|
||||
eqTimePtr(n.LastSeen, n2.LastSeen) &&
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig; DO NOT EDIT.
|
||||
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo; DO NOT EDIT.
|
||||
|
||||
package tailcfg
|
||||
|
||||
@@ -73,62 +73,3 @@ func (src *NetInfo) Clone() *NetInfo {
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Group.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Group) Clone() *Group {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Group)
|
||||
*dst = *src
|
||||
dst.Members = append(src.Members[:0:0], src.Members...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Role.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Role) Clone() *Role {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Role)
|
||||
*dst = *src
|
||||
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Capability.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Capability) Clone() *Capability {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Capability)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Login.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Login) Clone() *Login {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Login)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of DNSConfig.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *DNSConfig) Clone() *DNSConfig {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(DNSConfig)
|
||||
*dst = *src
|
||||
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
|
||||
dst.Domains = append(src.Domains[:0:0], src.Domains...)
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -315,11 +315,6 @@ func TestNodeEqual(t *testing.T) {
|
||||
&Node{LastSeen: &now},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{DERP: "foo"},
|
||||
&Node{DERP: "bar"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equal(tt.b)
|
||||
|
||||
@@ -7,10 +7,7 @@ package tstest
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type testLogWriter struct {
|
||||
@@ -44,48 +41,3 @@ func (panicLogWriter) Write(b []byte) (int, error) {
|
||||
func PanicOnLog() {
|
||||
log.SetOutput(panicLogWriter{})
|
||||
}
|
||||
|
||||
// ListenFor produces a LogListener wrapping a given logf with the given logStrings
|
||||
func ListenFor(logf logger.Logf, logStrings []string) *LogListener {
|
||||
ret := LogListener{
|
||||
logf: logf,
|
||||
listenFor: logStrings,
|
||||
seen: make(map[string]bool),
|
||||
}
|
||||
for _, line := range logStrings {
|
||||
ret.seen[line] = false
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
// LogListener takes a list of log lines to listen for
|
||||
type LogListener struct {
|
||||
logf logger.Logf
|
||||
listenFor []string
|
||||
|
||||
mu sync.Mutex
|
||||
seen map[string]bool
|
||||
}
|
||||
|
||||
// Logf records and logs a given line
|
||||
func (ll *LogListener) Logf(format string, args ...interface{}) {
|
||||
ll.mu.Lock()
|
||||
if _, ok := ll.seen[format]; ok {
|
||||
ll.seen[format] = true
|
||||
}
|
||||
ll.mu.Unlock()
|
||||
ll.logf(format, args)
|
||||
}
|
||||
|
||||
// Check returns which lines haven't been logged yet
|
||||
func (ll *LogListener) Check() []string {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
var notSeen []string
|
||||
for _, line := range ll.listenFor {
|
||||
if !ll.seen[line] {
|
||||
notSeen = append(notSeen, line)
|
||||
}
|
||||
}
|
||||
return notSeen
|
||||
}
|
||||
|
||||
@@ -1,46 +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.
|
||||
|
||||
package tstest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLogListener(t *testing.T) {
|
||||
var (
|
||||
l1 = "line 1: %s"
|
||||
l2 = "line 2: %s"
|
||||
l3 = "line 3: %s"
|
||||
|
||||
lineList = []string{l1, l2}
|
||||
)
|
||||
|
||||
ll := ListenFor(t.Logf, lineList)
|
||||
|
||||
if len(ll.Check()) != len(lineList) {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l3, "hi")
|
||||
|
||||
if len(ll.Check()) != len(lineList) {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l1, "hi")
|
||||
|
||||
if len(ll.Check()) != len(lineList)-1 {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l1, "bye")
|
||||
|
||||
if len(ll.Check()) != len(lineList)-1 {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l2, "hi")
|
||||
if ll.Check() != nil {
|
||||
t.Errorf("expected empty list, got ll.Check()")
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func goroutineDump() (int, string) {
|
||||
return p.Count(), b.String()
|
||||
}
|
||||
|
||||
func (r *ResourceCheck) Assert(t testing.TB) {
|
||||
func (r *ResourceCheck) Assert(t *testing.T) {
|
||||
t.Helper()
|
||||
want := r.startNumRoutines
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package tsweb
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
@@ -15,59 +16,119 @@ type response struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: Header
|
||||
func responseSuccess(data interface{}) *response {
|
||||
return &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// JSONHandlerFunc only take *http.Request as argument to avoid any misuse of http.ResponseWriter.
|
||||
// The function's results must be (status int, data interface{}, err error).
|
||||
// Return a HTTPError to show an error message, otherwise JSONHandler will only show "internal server error".
|
||||
type JSONHandlerFunc func(r *http.Request) (status int, data interface{}, err error)
|
||||
func responseError(e string) *response {
|
||||
return &response{
|
||||
Status: "error",
|
||||
Error: e,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP calls the JSONHandlerFunc and automatically marshals http responses.
|
||||
//
|
||||
// Use the following code to unmarshal the request body
|
||||
// body := new(DataType)
|
||||
// if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
// return http.StatusBadRequest, nil, err
|
||||
// }
|
||||
//
|
||||
// Check jsonhandler_text.go for examples
|
||||
func (fn JSONHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func writeResponse(w http.ResponseWriter, s int, resp *response) {
|
||||
b, _ := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
var resp *response
|
||||
status, data, err := fn(r)
|
||||
if status == 0 {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
} else if err == nil {
|
||||
resp = &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
if werr, ok := err.(HTTPError); ok {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: werr.Msg,
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"status":"error","error":"json marshal error"}`))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.WriteHeader(s)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func checkFn(t reflect.Type) {
|
||||
h := reflect.TypeOf(http.HandlerFunc(nil))
|
||||
switch t.NumIn() {
|
||||
case 2, 3:
|
||||
if !t.In(0).AssignableTo(h.In(0)) {
|
||||
panic("first argument must be http.ResponseWriter")
|
||||
}
|
||||
if !t.In(1).AssignableTo(h.In(1)) {
|
||||
panic("second argument must be *http.Request")
|
||||
}
|
||||
default:
|
||||
panic("JSONHandler: number of input parameter should be 2 or 3")
|
||||
}
|
||||
|
||||
switch t.NumOut() {
|
||||
case 1:
|
||||
if !t.Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||
panic("return value must be error")
|
||||
}
|
||||
case 2:
|
||||
if !t.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||
panic("second return value must be error")
|
||||
}
|
||||
default:
|
||||
panic("JSONHandler: number of return values should be 1 or 2")
|
||||
}
|
||||
}
|
||||
|
||||
// JSONHandler wraps an HTTP handler function with a version that automatically
|
||||
// unmarshals and marshals requests and responses respectively into fn's arguments
|
||||
// and results.
|
||||
//
|
||||
// The fn parameter is a function. It must take two or three input arguments.
|
||||
// The first two arguments must be http.ResponseWriter and *http.Request.
|
||||
// The optional third argument can be of any type representing the JSON input.
|
||||
// The function's results can be either (error) or (T, error), where T is the
|
||||
// JSON-marshalled result type.
|
||||
//
|
||||
// For example:
|
||||
// fn := func(w http.ResponseWriter, r *http.Request, in *Req) (*Res, error) { ... }
|
||||
func JSONHandler(fn interface{}) http.Handler {
|
||||
v := reflect.ValueOf(fn)
|
||||
t := v.Type()
|
||||
checkFn(t)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wv := reflect.ValueOf(w)
|
||||
rv := reflect.ValueOf(r)
|
||||
var vs []reflect.Value
|
||||
|
||||
switch t.NumIn() {
|
||||
case 2:
|
||||
vs = v.Call([]reflect.Value{wv, rv})
|
||||
case 3:
|
||||
dv := reflect.New(t.In(2))
|
||||
err := json.NewDecoder(r.Body).Decode(dv.Interface())
|
||||
if err != nil {
|
||||
writeResponse(w, http.StatusBadRequest, responseError("bad json"))
|
||||
return
|
||||
}
|
||||
vs = v.Call([]reflect.Value{wv, rv, dv.Elem()})
|
||||
default:
|
||||
panic("JSONHandler: number of input parameter should be 2 or 3")
|
||||
}
|
||||
|
||||
var e reflect.Value
|
||||
switch len(vs) {
|
||||
case 1:
|
||||
// todo support other error types
|
||||
if vs[0].IsZero() {
|
||||
writeResponse(w, http.StatusOK, responseSuccess(nil))
|
||||
return
|
||||
}
|
||||
e = vs[0]
|
||||
case 2:
|
||||
if vs[1].IsZero() {
|
||||
if !vs[0].IsZero() {
|
||||
writeResponse(w, http.StatusOK, responseSuccess(vs[0].Interface()))
|
||||
}
|
||||
return
|
||||
}
|
||||
e = vs[1]
|
||||
default:
|
||||
panic("JSONHandler: number of return values should be 1 or 2")
|
||||
}
|
||||
|
||||
if e.Type().AssignableTo(reflect.TypeOf(HTTPError{})) {
|
||||
err := e.Interface().(HTTPError)
|
||||
writeResponse(w, err.Code, responseError(err.Error()))
|
||||
} else {
|
||||
err := e.Interface().(error)
|
||||
writeResponse(w, http.StatusBadRequest, responseError(err.Error()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
package tsweb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -25,7 +26,7 @@ type Response struct {
|
||||
}
|
||||
|
||||
func TestNewJSONHandler(t *testing.T) {
|
||||
checkStatus := func(w *httptest.ResponseRecorder, status string, code int) *Response {
|
||||
checkStatus := func(w *httptest.ResponseRecorder, status string) *Response {
|
||||
d := &Response{
|
||||
Data: &Data{},
|
||||
}
|
||||
@@ -43,10 +44,6 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Fatalf("wrong status: %s %s", d.Status, status)
|
||||
}
|
||||
|
||||
if w.Code != code {
|
||||
t.Fatalf("wrong status code: %d %d", w.Code, code)
|
||||
}
|
||||
|
||||
if w.Header().Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("wrong content type: %s", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@@ -54,139 +51,163 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
return d
|
||||
}
|
||||
|
||||
h21 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, nil, nil
|
||||
// 2 1
|
||||
h21 := JSONHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
t.Run("200 simple", func(t *testing.T) {
|
||||
t.Run("2 1 simple", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h21.ServeHTTP(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
checkStatus(w, "success")
|
||||
})
|
||||
|
||||
t.Run("403 HTTPError", func(t *testing.T) {
|
||||
h := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusForbidden, nil, fmt.Errorf("forbidden")
|
||||
t.Run("2 1 HTTPError", func(t *testing.T) {
|
||||
h := JSONHandler(func(w http.ResponseWriter, r *http.Request) HTTPError {
|
||||
return Error(http.StatusForbidden, "forbidden", nil)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
checkStatus(w, "error", http.StatusForbidden)
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("wrong code: %d %d", w.Code, http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
|
||||
h22 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, &Data{Name: "tailscale"}, nil
|
||||
// 2 2
|
||||
h22 := JSONHandler(func(w http.ResponseWriter, r *http.Request) (*Data, error) {
|
||||
return &Data{Name: "tailscale"}, nil
|
||||
})
|
||||
|
||||
t.Run("200 get data", func(t *testing.T) {
|
||||
t.Run("2 2 get data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h22.ServeHTTP(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
checkStatus(w, "success")
|
||||
})
|
||||
|
||||
h31 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
body := new(Data)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
return http.StatusBadRequest, nil, err
|
||||
// 3 1
|
||||
h31 := JSONHandler(func(w http.ResponseWriter, r *http.Request, d *Data) error {
|
||||
if d.Name == "" {
|
||||
return errors.New("name is empty")
|
||||
}
|
||||
|
||||
if body.Name == "" {
|
||||
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "name is empty", nil)
|
||||
}
|
||||
|
||||
return http.StatusOK, nil, nil
|
||||
return nil
|
||||
})
|
||||
t.Run("200 post data", func(t *testing.T) {
|
||||
t.Run("3 1 post data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "tailscale"}`))
|
||||
h31.ServeHTTP(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
checkStatus(w, "success")
|
||||
})
|
||||
|
||||
t.Run("400 bad json", func(t *testing.T) {
|
||||
t.Run("3 1 bad json", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{`))
|
||||
h31.ServeHTTP(w, r)
|
||||
checkStatus(w, "error", http.StatusBadRequest)
|
||||
checkStatus(w, "error")
|
||||
})
|
||||
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
t.Run("3 1 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h31.ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
resp := checkStatus(w, "error")
|
||||
if resp.Error != "name is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
})
|
||||
|
||||
h32 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
body := new(Data)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
return http.StatusBadRequest, nil, err
|
||||
}
|
||||
if body.Name == "root" {
|
||||
return http.StatusInternalServerError, nil, fmt.Errorf("invalid name")
|
||||
}
|
||||
if body.Price == 0 {
|
||||
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "price is empty", nil)
|
||||
// 3 2
|
||||
h32 := JSONHandler(func(w http.ResponseWriter, r *http.Request, d *Data) (*Data, error) {
|
||||
if d.Price == 0 {
|
||||
return nil, errors.New("price is empty")
|
||||
}
|
||||
|
||||
return http.StatusOK, &Data{Price: body.Price * 2}, nil
|
||||
return &Data{Price: d.Price * 2}, nil
|
||||
})
|
||||
|
||||
t.Run("200 post data", func(t *testing.T) {
|
||||
t.Run("3 2 post data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Price": 10}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "success", http.StatusOK)
|
||||
resp := checkStatus(w, "success")
|
||||
t.Log(resp.Data)
|
||||
if resp.Data.Price != 20 {
|
||||
t.Fatalf("wrong price: %d %d", resp.Data.Price, 10)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
t.Run("3 2 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
resp := checkStatus(w, "error")
|
||||
if resp.Error != "price is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("500 internal server error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "root"}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "internal server error" {
|
||||
t.Fatalf("wrong error")
|
||||
// fn check
|
||||
shouldPanic := func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatalf("should panic")
|
||||
}
|
||||
t.Log(r)
|
||||
}
|
||||
|
||||
t.Run("2 0 panic", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(w http.ResponseWriter, r *http.Request) {})
|
||||
})
|
||||
|
||||
t.Run("500 misuse", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, make(chan int), nil
|
||||
}).ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "json marshal error" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
t.Run("2 1 panic return value", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(w http.ResponseWriter, r *http.Request) string {
|
||||
return ""
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("500 empty status code", func(t *testing.T) {
|
||||
t.Run("2 1 panic arguments", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(r *http.Request, w http.ResponseWriter) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("3 1 panic arguments", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(name string, r *http.Request, w http.ResponseWriter) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("3 2 panic return value", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
//lint:ignore ST1008 intentional
|
||||
JSONHandler(func(name string, r *http.Request, w http.ResponseWriter) (error, string) {
|
||||
return nil, "panic"
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("2 2 forbidden", func(t *testing.T) {
|
||||
code := http.StatusForbidden
|
||||
body := []byte("forbidden")
|
||||
h := JSONHandler(func(w http.ResponseWriter, r *http.Request) (*Data, error) {
|
||||
w.WriteHeader(code)
|
||||
w.Write(body)
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (status int, data interface{}, err error) {
|
||||
return
|
||||
}).ServeHTTP(w, r)
|
||||
checkStatus(w, "error", http.StatusInternalServerError)
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("wrong code: %d %d", w.Code, code)
|
||||
}
|
||||
if !bytes.Equal(w.Body.Bytes(), []byte("forbidden")) {
|
||||
t.Fatalf("wrong body: %s %s", w.Body.Bytes(), body)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ func NewPrivate() Private {
|
||||
if _, err := io.ReadFull(crand.Reader, p[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p[0] &= 248
|
||||
p[31] = (p[31] & 127) | 64
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ package key
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
)
|
||||
|
||||
func TestTextUnmarshal(t *testing.T) {
|
||||
@@ -24,31 +22,3 @@ func TestTextUnmarshal(t *testing.T) {
|
||||
t.Fatalf("mismatch; got %x want %x", p2, p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClamping(t *testing.T) {
|
||||
t.Run("NewPrivate", func(t *testing.T) { testClamping(t, NewPrivate) })
|
||||
|
||||
// Also test the wgcfg package, as their behavior should match.
|
||||
t.Run("wgcfg", func(t *testing.T) {
|
||||
testClamping(t, func() Private {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Private(k)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testClamping(t *testing.T, newKey func() Private) {
|
||||
for i := 0; i < 100; i++ {
|
||||
k := newKey()
|
||||
if k[0]&0b111 != 0 {
|
||||
t.Fatalf("Bogus clamping in first byte: %#08b", k[0])
|
||||
return
|
||||
}
|
||||
if k[31]>>6 != 1 {
|
||||
t.Fatalf("Bogus clamping in last byte: %#08b", k[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
// Package version provides the version that the binary was built at.
|
||||
package version
|
||||
|
||||
const LONG = "date.20200820"
|
||||
const LONG = "date.20200806"
|
||||
const SHORT = LONG
|
||||
|
||||
@@ -7,13 +7,11 @@ package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/packet"
|
||||
)
|
||||
@@ -131,79 +129,6 @@ func maybeHexdump(flag RunFlags, b []byte) string {
|
||||
return packet.Hexdump(b) + "\n"
|
||||
}
|
||||
|
||||
// MatchesFromFilterRules parse a number of wire-format FilterRule values into
|
||||
// the Matches format.
|
||||
// If an error is returned, the Matches result is still valid, containing the rules that
|
||||
// were successfully converted.
|
||||
func MatchesFromFilterRules(pf []tailcfg.FilterRule) (Matches, error) {
|
||||
mm := make([]Match, 0, len(pf))
|
||||
var erracc error
|
||||
|
||||
for _, r := range pf {
|
||||
m := Match{}
|
||||
|
||||
for i, s := range r.SrcIPs {
|
||||
bits := 32
|
||||
if len(r.SrcBits) > i {
|
||||
bits = r.SrcBits[i]
|
||||
}
|
||||
net, err := parseIP(s, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Srcs = append(m.Srcs, net)
|
||||
}
|
||||
|
||||
for _, d := range r.DstPorts {
|
||||
bits := 32
|
||||
if d.Bits != nil {
|
||||
bits = *d.Bits
|
||||
}
|
||||
net, err := parseIP(d.IP, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Dsts = append(m.Dsts, NetPortRange{
|
||||
Net: net,
|
||||
Ports: PortRange{
|
||||
First: d.Ports.First,
|
||||
Last: d.Ports.Last,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
mm = append(mm, m)
|
||||
}
|
||||
return mm, erracc
|
||||
}
|
||||
|
||||
func parseIP(host string, defaultBits int) (Net, error) {
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil && ip.IsUnspecified() {
|
||||
// For clarity, reject 0.0.0.0 as an input
|
||||
return NetNone, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host)
|
||||
} else if ip == nil && host == "*" {
|
||||
// User explicitly requested wildcard dst ip
|
||||
return NetAny, nil
|
||||
} else {
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
if ip == nil || len(ip) != 4 {
|
||||
return NetNone, fmt.Errorf("ports=%#v: invalid IPv4 address", host)
|
||||
}
|
||||
if len(ip) == 4 && (defaultBits < 0 || defaultBits > 32) {
|
||||
return NetNone, fmt.Errorf("invalid CIDR size %d for host %q", defaultBits, host)
|
||||
}
|
||||
return Net{
|
||||
IP: NewIP(ip),
|
||||
Mask: Netmask(defaultBits),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(apenwarr): use a bigger bucket for specifically TCP SYN accept logging?
|
||||
// Logging is a quick way to record every newly opened TCP connection, but
|
||||
// we have to be cautious about flooding the logs vs letting people use
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -163,35 +162,6 @@ func TestNoAllocs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
bits int
|
||||
want Net
|
||||
wantErr string
|
||||
}{
|
||||
{"8.8.8.8", 24, Net{IP: packet.NewIP(net.ParseIP("8.8.8.8")), Mask: packet.NewIP(net.ParseIP("255.255.255.0"))}, ""},
|
||||
{"8.8.8.8", 33, Net{}, `invalid CIDR size 33 for host "8.8.8.8"`},
|
||||
{"8.8.8.8", -1, Net{}, `invalid CIDR size -1 for host "8.8.8.8"`},
|
||||
{"0.0.0.0", 24, Net{}, `ports="0.0.0.0": to allow all IP addresses, use *:port, not 0.0.0.0:port`},
|
||||
{"*", 24, NetAny, ""},
|
||||
{"fe80::1", 128, NetNone, `ports="fe80::1": invalid IPv4 address`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := parseIP(tt.host, tt.bits)
|
||||
if err != nil {
|
||||
if err.Error() == tt.wantErr {
|
||||
continue
|
||||
}
|
||||
t.Errorf("parseIP(%q, %v) error: %v; want error %q", tt.host, tt.bits, err, tt.wantErr)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("parseIP(%q, %v) = %#v; want %#v", tt.host, tt.bits, got, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
acl := newFilter(b.Logf)
|
||||
|
||||
|
||||
@@ -70,8 +70,7 @@ var (
|
||||
// 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)
|
||||
debugUseDerpRoute, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ENABLE_DERP_ROUTE"))
|
||||
// logDerpVerbose logs all received DERP packets, including their
|
||||
// full payload.
|
||||
logDerpVerbose, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DERP"))
|
||||
@@ -82,19 +81,6 @@ var (
|
||||
debugReSTUNStopOnIdle, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_RESTUN_STOP_ON_IDLE"))
|
||||
)
|
||||
|
||||
// useDerpRoute reports whether magicsock should enable the DERP
|
||||
// return path optimization (Issue 150).
|
||||
func useDerpRoute() bool {
|
||||
if debugUseDerpRouteEnv != "" {
|
||||
return debugUseDerpRoute
|
||||
}
|
||||
ob := controlclient.DERPRouteFlag()
|
||||
if v, ok := ob.Get(); ok {
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// inTest reports whether the running program is a test that set the
|
||||
// IN_TS_TEST environment variable.
|
||||
//
|
||||
@@ -113,7 +99,6 @@ type Conn struct {
|
||||
pconn4 *RebindingUDPConn
|
||||
pconn6 *RebindingUDPConn // non-nil if IPv6 available
|
||||
epFunc func(endpoints []string)
|
||||
derpActiveFunc func()
|
||||
logf logger.Logf
|
||||
sendLogLimit *rate.Limiter
|
||||
netChecker *netcheck.Client
|
||||
@@ -282,10 +267,6 @@ type Options struct {
|
||||
// endpoints change. The called func does not own the slice.
|
||||
EndpointsFunc func(endpoint []string)
|
||||
|
||||
// DERPActiveFunc optionally provides a func to be called when
|
||||
// a connection is made to a DERP server.
|
||||
DERPActiveFunc func()
|
||||
|
||||
// IdleFunc optionally provides a func to return how long
|
||||
// it's been since a TUN packet was sent or received.
|
||||
IdleFunc func() time.Duration
|
||||
@@ -322,13 +303,6 @@ func (o *Options) endpointsFunc() func([]string) {
|
||||
return o.EndpointsFunc
|
||||
}
|
||||
|
||||
func (o *Options) derpActiveFunc() func() {
|
||||
if o == nil || o.DERPActiveFunc == nil {
|
||||
return func() {}
|
||||
}
|
||||
return o.DERPActiveFunc
|
||||
}
|
||||
|
||||
// newConn is the error-free, network-listening-side-effect-free based
|
||||
// of NewConn. Mostly for tests.
|
||||
func newConn() *Conn {
|
||||
@@ -358,7 +332,6 @@ func NewConn(opts Options) (*Conn, error) {
|
||||
c.pconnPort = opts.Port
|
||||
c.logf = opts.logf()
|
||||
c.epFunc = opts.endpointsFunc()
|
||||
c.derpActiveFunc = opts.derpActiveFunc()
|
||||
c.idleFunc = opts.IdleFunc
|
||||
c.packetListener = opts.PacketListener
|
||||
c.noteRecvActivity = opts.NoteRecvActivity
|
||||
@@ -594,107 +567,6 @@ func (c *Conn) SetNetInfoCallback(fn func(*tailcfg.NetInfo)) {
|
||||
}
|
||||
}
|
||||
|
||||
// peerForIP returns the Node in nm that's responsible for
|
||||
// handling the given IP address.
|
||||
func peerForIP(nm *controlclient.NetworkMap, ip netaddr.IP) (n *tailcfg.Node, ok bool) {
|
||||
if nm == nil {
|
||||
return nil, false
|
||||
}
|
||||
wgIP := wgcfg.IP{Addr: ip.As16()}
|
||||
// Check for exact matches before looking for subnet matches.
|
||||
for _, p := range nm.Peers {
|
||||
for _, a := range p.Addresses {
|
||||
if a.IP == wgIP {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
for _, cidr := range p.AllowedIPs {
|
||||
if cidr.Contains(wgIP) {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Ping handles a "tailscale ping" CLI query.
|
||||
func (c *Conn) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
res := &ipnstate.PingResult{IP: ip.String()}
|
||||
if c.privateKey.IsZero() {
|
||||
res.Err = "local tailscaled stopped"
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
peer, ok := peerForIP(c.netMap, ip)
|
||||
if !ok {
|
||||
res.Err = "no matching peer"
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
if len(peer.Addresses) > 0 {
|
||||
res.NodeIP = peer.Addresses[0].IP.String()
|
||||
}
|
||||
res.NodeName = peer.Name // prefer DNS name
|
||||
if res.NodeName == "" {
|
||||
res.NodeName = peer.Hostinfo.Hostname // else hostname
|
||||
} else {
|
||||
if i := strings.Index(res.NodeName, "."); i != -1 {
|
||||
res.NodeName = res.NodeName[:i]
|
||||
}
|
||||
}
|
||||
|
||||
dk, ok := c.discoOfNode[peer.Key]
|
||||
if !ok {
|
||||
res.Err = "no discovery key for peer (pre 0.100?)"
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
de, ok := c.endpointOfDisco[dk]
|
||||
if !ok {
|
||||
c.mu.Unlock() // temporarily release
|
||||
if c.noteRecvActivity != nil {
|
||||
c.noteRecvActivity(dk)
|
||||
}
|
||||
c.mu.Lock() // re-acquire
|
||||
|
||||
// re-check at least basic invariant:
|
||||
if c.privateKey.IsZero() {
|
||||
res.Err = "local tailscaled stopped"
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
|
||||
de, ok = c.endpointOfDisco[dk]
|
||||
if !ok {
|
||||
res.Err = "internal error: failed to create endpoint for discokey"
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
c.logf("magicsock: started peer %v for ping to %v", dk.ShortString(), peer.Key.ShortString())
|
||||
}
|
||||
de.cliPing(res, cb)
|
||||
}
|
||||
|
||||
// c.mu must be held
|
||||
func (c *Conn) populateCLIPingResponseLocked(res *ipnstate.PingResult, latency time.Duration, ep netaddr.IPPort) {
|
||||
res.LatencySeconds = latency.Seconds()
|
||||
if ep.IP != derpMagicIPAddr {
|
||||
res.Endpoint = ep.String()
|
||||
return
|
||||
}
|
||||
regionID := int(ep.Port)
|
||||
res.DERPRegionID = regionID
|
||||
if c.derpMap != nil {
|
||||
if dr, ok := c.derpMap.Regions[regionID]; ok {
|
||||
res.DERPRegionCode = dr.RegionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DiscoPublicKey returns the discovery public key.
|
||||
func (c *Conn) DiscoPublicKey() tailcfg.DiscoKey {
|
||||
c.mu.Lock()
|
||||
@@ -1138,7 +1010,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
|
||||
// perhaps peer's home is Frankfurt, but they dialed our home DERP
|
||||
// node in SF to reach us, so we can reply to them using our
|
||||
// SF connection rather than dialing Frankfurt. (Issue 150)
|
||||
if !peer.IsZero() && useDerpRoute() {
|
||||
if !peer.IsZero() && debugUseDerpRoute {
|
||||
if r, ok := c.derpRoute[peer]; ok {
|
||||
if ad, ok := c.activeDerp[r.derpID]; ok && ad.c == r.dc {
|
||||
c.setPeerLastDerpLocked(peer, r.derpID, regionID)
|
||||
@@ -1219,7 +1091,6 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
|
||||
|
||||
go c.runDerpReader(ctx, addr, dc, wg, startGate)
|
||||
go c.runDerpWriter(ctx, dc, ch, wg, startGate)
|
||||
go c.derpActiveFunc()
|
||||
|
||||
return ad.writeCh
|
||||
}
|
||||
@@ -1765,7 +1636,6 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
|
||||
needsRecvActivityCall = de.isFirstRecvActivityInAwhile()
|
||||
}
|
||||
if needsRecvActivityCall && c.noteRecvActivity != nil {
|
||||
c.logf("magicsock: got disco message from idle peer, starting lazy conf for %v, %v", peerNode.Key.ShortString(), sender.ShortString())
|
||||
// We can't hold Conn.mu while calling noteRecvActivity.
|
||||
// noteRecvActivity acquires userspaceEngine.wgLock (and per our
|
||||
// lock ordering rules: wgLock must come first), and also calls
|
||||
@@ -1854,21 +1724,27 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// de may be nil
|
||||
func (c *Conn) handlePingLocked(dm *disco.Ping, de *discoEndpoint, src netaddr.IPPort, sender tailcfg.DiscoKey, peerNode *tailcfg.Node) {
|
||||
if peerNode == nil {
|
||||
c.logf("magicsock: disco: [unexpected] ignoring ping from unknown peer Node")
|
||||
return
|
||||
}
|
||||
likelyHeartBeat := src == de.lastPingFrom && time.Since(de.lastPingTime) < 5*time.Second
|
||||
de.lastPingFrom = src
|
||||
de.lastPingTime = time.Now()
|
||||
likelyHeartBeat := de != nil && src == de.lastPingFrom && time.Since(de.lastPingTime) < 5*time.Second
|
||||
var discoShort string
|
||||
if de != nil {
|
||||
discoShort = de.discoShort
|
||||
de.lastPingFrom = src
|
||||
de.lastPingTime = time.Now()
|
||||
} else {
|
||||
discoShort = sender.ShortString()
|
||||
}
|
||||
if !likelyHeartBeat || debugDisco {
|
||||
c.logf("magicsock: disco: %v<-%v (%v, %v) got ping tx=%x", c.discoShort, de.discoShort, peerNode.Key.ShortString(), src, dm.TxID[:6])
|
||||
c.logf("magicsock: disco: %v<-%v (%v, %v) got ping tx=%x", c.discoShort, discoShort, peerNode.Key.ShortString(), src, dm.TxID[:6])
|
||||
}
|
||||
|
||||
// Remember this route if not present.
|
||||
c.setAddrToDiscoLocked(src, sender, nil)
|
||||
de.addCandidateEndpoint(src)
|
||||
|
||||
ipDst := src
|
||||
discoDest := sender
|
||||
@@ -2782,7 +2658,7 @@ func (c *Conn) CreateBind(uint16) (conn.Bind, uint16, error) {
|
||||
//
|
||||
// The key is the public key of the peer and addrs is either:
|
||||
//
|
||||
// 1) a comma-separated list of UDP ip:ports (the peer doesn't have a discovery key)
|
||||
// 1) a comma-separated list of UDP ip:ports (the the peer doesn't have a discovery key)
|
||||
// 2) "<hex-discovery-key>.disco.tailscale:12345", a magic value that means the peer
|
||||
// is running code that supports active discovery, so CreateEndpoint returns
|
||||
// a discoEndpoint.
|
||||
@@ -3035,21 +2911,6 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
ss := &ipnstate.PeerStatus{
|
||||
PublicKey: c.privateKey.Public(),
|
||||
Addrs: c.lastEndpoints,
|
||||
}
|
||||
if c.netMap != nil {
|
||||
ss.HostName = c.netMap.Hostinfo.Hostname
|
||||
ss.OS = c.netMap.Hostinfo.OS
|
||||
}
|
||||
if c.derpMap != nil {
|
||||
derpRegion, ok := c.derpMap.Regions[c.myDerp]
|
||||
if ok {
|
||||
ss.Relay = derpRegion.RegionCode
|
||||
}
|
||||
}
|
||||
|
||||
if c.netMap != nil {
|
||||
for _, addr := range c.netMap.Addresses {
|
||||
if (addr.IP.Is4() && addr.Mask != 32) || (addr.IP.Is6() && addr.Mask != 128) {
|
||||
@@ -3057,11 +2918,9 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
}
|
||||
if ip, ok := netaddr.FromStdIP(addr.IP.IP()); ok {
|
||||
sb.AddTailscaleIP(ip)
|
||||
ss.TailAddr = ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.SetSelfStatus(ss)
|
||||
|
||||
for dk, n := range c.nodeOfDisco {
|
||||
ps := &ipnstate.PeerStatus{InMagicSock: true}
|
||||
@@ -3128,13 +2987,6 @@ type discoEndpoint struct {
|
||||
trustBestAddrUntil time.Time // time when bestAddr expires
|
||||
sentPing map[stun.TxID]sentPing
|
||||
endpointState map[netaddr.IPPort]*endpointState
|
||||
|
||||
pendingCLIPings []pendingCLIPing // any outstanding "tailscale ping" commands running
|
||||
}
|
||||
|
||||
type pendingCLIPing struct {
|
||||
res *ipnstate.PingResult
|
||||
cb func(*ipnstate.PingResult)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -3173,44 +3025,11 @@ const (
|
||||
// a discoEndpoint. (The subject is the discoEndpoint.endpointState
|
||||
// map key)
|
||||
type endpointState struct {
|
||||
// all fields guarded by discoEndpoint.mu
|
||||
|
||||
// lastPing is the last (outgoing) ping time.
|
||||
lastPing time.Time
|
||||
|
||||
// lastGotPing, if non-zero, means that this was an endpoint
|
||||
// that we learned about at runtime (from an incoming ping)
|
||||
// and that is not in the network map. If so, we keep the time
|
||||
// updated and use it to discard old candidates.
|
||||
lastGotPing time.Time
|
||||
|
||||
// all fields guarded by discoEndpoint.mu:
|
||||
lastPing time.Time
|
||||
recentPongs []pongReply // ring buffer up to pongHistoryCount entries
|
||||
recentPong uint16 // index into recentPongs of most recent; older before, wrapped
|
||||
|
||||
index int16 // index in nodecfg.Node.Endpoints; meaningless if lastGotPing non-zero
|
||||
}
|
||||
|
||||
// indexSentinelDeleted is the temporary value that endpointState.index takes while
|
||||
// a discoEndpoint's endpoints are being updated from a new network map.
|
||||
const indexSentinelDeleted = -1
|
||||
|
||||
// shouldDeleteLocked reports whether we should delete this endpoint.
|
||||
func (st *endpointState) shouldDeleteLocked() bool {
|
||||
switch {
|
||||
case st.lastGotPing.IsZero():
|
||||
// This was an endpoint from the network map. Is it still in the network map?
|
||||
return st.index == indexSentinelDeleted
|
||||
default:
|
||||
// Thiw was an endpoint discovered at runtime.
|
||||
return time.Since(st.lastGotPing) > sessionActiveTimeout
|
||||
}
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) deleteEndpointLocked(ep netaddr.IPPort) {
|
||||
delete(de.endpointState, ep)
|
||||
if de.bestAddr == ep {
|
||||
de.bestAddr = netaddr.IPPort{}
|
||||
}
|
||||
recentPong uint16 // index into recentPongs of most recent; older , wrapped
|
||||
index int16 // index in nodecfg.Node.Endpoints
|
||||
}
|
||||
|
||||
// pongHistoryCount is how many pongReply values we keep per endpointState
|
||||
@@ -3367,33 +3186,6 @@ func (de *discoEndpoint) noteActiveLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
// cliPing starts a ping for the "tailscale ping" command. res is value to call cb with,
|
||||
// already partially filled.
|
||||
func (de *discoEndpoint) cliPing(res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
de.pendingCLIPings = append(de.pendingCLIPings, pendingCLIPing{res, cb})
|
||||
|
||||
now := time.Now()
|
||||
udpAddr, derpAddr := de.addrForSendLocked(now)
|
||||
if !derpAddr.IsZero() {
|
||||
de.startPingLocked(derpAddr, now, pingCLI)
|
||||
}
|
||||
if !udpAddr.IsZero() && now.Before(de.trustBestAddrUntil) {
|
||||
// Already have an active session, so just ping the address we're using.
|
||||
// Otherwise "tailscale ping" results to a node on the local network
|
||||
// can look like they're bouncing between, say 10.0.0.0/9 and the peer's
|
||||
// IPv6 address, both 1ms away, and it's random who replies first.
|
||||
de.startPingLocked(udpAddr, now, pingCLI)
|
||||
} else {
|
||||
for ep := range de.endpointState {
|
||||
de.startPingLocked(ep, now, pingCLI)
|
||||
}
|
||||
}
|
||||
de.noteActiveLocked()
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) send(b []byte) error {
|
||||
now := time.Now()
|
||||
|
||||
@@ -3473,23 +3265,17 @@ const (
|
||||
// pingHeartbeat means that purpose of a ping was whether a
|
||||
// peer was still there.
|
||||
pingHeartbeat
|
||||
|
||||
// pingCLI means that the user is running "tailscale ping"
|
||||
// from the CLI. These types of pings can go over DERP.
|
||||
pingCLI
|
||||
)
|
||||
|
||||
func (de *discoEndpoint) startPingLocked(ep netaddr.IPPort, now time.Time, purpose discoPingPurpose) {
|
||||
if purpose != pingCLI {
|
||||
st, ok := de.endpointState[ep]
|
||||
if !ok {
|
||||
// Shouldn't happen. But don't ping an endpoint that's
|
||||
// not active for us.
|
||||
de.c.logf("magicsock: disco: [unexpected] attempt to ping no longer live endpoint %v", ep)
|
||||
return
|
||||
}
|
||||
st.lastPing = now
|
||||
st, ok := de.endpointState[ep]
|
||||
if !ok {
|
||||
// Shouldn't happen. But don't ping an endpoint that's
|
||||
// not active for us.
|
||||
de.c.logf("magicsock: disco: [unexpected] attempt to ping no longer live endpoint %v", ep)
|
||||
return
|
||||
}
|
||||
st.lastPing = now
|
||||
|
||||
txid := stun.NewTxID()
|
||||
de.sentPing[txid] = sentPing{
|
||||
@@ -3509,10 +3295,7 @@ func (de *discoEndpoint) sendPingsLocked(now time.Time, sendCallMeMaybe bool) {
|
||||
de.lastFullPing = now
|
||||
var sentAny bool
|
||||
for ep, st := range de.endpointState {
|
||||
if st.shouldDeleteLocked() {
|
||||
de.deleteEndpointLocked(ep)
|
||||
continue
|
||||
}
|
||||
ep := ep
|
||||
if !st.lastPing.IsZero() && now.Sub(st.lastPing) < discoPingInterval {
|
||||
continue
|
||||
}
|
||||
@@ -3557,7 +3340,7 @@ func (de *discoEndpoint) updateFromNode(n *tailcfg.Node) {
|
||||
}
|
||||
|
||||
for _, st := range de.endpointState {
|
||||
st.index = indexSentinelDeleted // assume deleted until updated in next loop
|
||||
st.index = -1 // assume deleted until updated in next loop
|
||||
}
|
||||
for i, epStr := range n.Endpoints {
|
||||
if i > math.MaxInt16 {
|
||||
@@ -3575,49 +3358,14 @@ func (de *discoEndpoint) updateFromNode(n *tailcfg.Node) {
|
||||
de.endpointState[ipp] = &endpointState{index: int16(i)}
|
||||
}
|
||||
}
|
||||
|
||||
// Now delete anything unless it's still in the network map or
|
||||
// was a recently discovered endpoint.
|
||||
for ep, st := range de.endpointState {
|
||||
if st.shouldDeleteLocked() {
|
||||
de.deleteEndpointLocked(ep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addCandidateEndpoint adds ep as an endpoint to which we should send
|
||||
// future pings.
|
||||
//
|
||||
// This is called once we've already verified that we got a valid
|
||||
// discovery message from de via ep.
|
||||
func (de *discoEndpoint) addCandidateEndpoint(ep netaddr.IPPort) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
if st, ok := de.endpointState[ep]; ok {
|
||||
if st.lastGotPing.IsZero() {
|
||||
// Already-known endpoint from the network map.
|
||||
return
|
||||
}
|
||||
st.lastGotPing = time.Now()
|
||||
return
|
||||
}
|
||||
|
||||
// Newly discovered endpoint. Exciting!
|
||||
de.c.logf("magicsock: disco: adding %v as candidate endpoint for %v (%s)", ep, de.discoShort, de.publicKey.ShortString())
|
||||
de.endpointState[ep] = &endpointState{
|
||||
lastGotPing: time.Now(),
|
||||
}
|
||||
|
||||
// If for some reason this gets very large, do some cleanup.
|
||||
if size := len(de.endpointState); size > 100 {
|
||||
for ep, st := range de.endpointState {
|
||||
if st.shouldDeleteLocked() {
|
||||
de.deleteEndpointLocked(ep)
|
||||
// Now delete anything that wasn't updated.
|
||||
for ipp, st := range de.endpointState {
|
||||
if st.index == -1 {
|
||||
delete(de.endpointState, ipp)
|
||||
if de.bestAddr == ipp {
|
||||
de.bestAddr = netaddr.IPPort{}
|
||||
}
|
||||
}
|
||||
size2 := len(de.endpointState)
|
||||
de.c.logf("magicsock: disco: addCandidateEndpoint pruned %v candidate set from %v to %v entries", size, size2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3637,7 +3385,14 @@ func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort)
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
isDerp := src.IP == derpMagicIPAddr
|
||||
if src.IP == derpMagicIPAddr {
|
||||
// We might support pinging a node via DERP in the
|
||||
// future to see if it's still there, but we don't
|
||||
// yet. We shouldn't ever get here, but bail out early
|
||||
// in case we do in the future. (In which case, hi!,
|
||||
// you'll be modifying this code.)
|
||||
return
|
||||
}
|
||||
|
||||
sp, ok := de.sentPing[m.TxID]
|
||||
if !ok {
|
||||
@@ -3646,25 +3401,23 @@ func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort)
|
||||
}
|
||||
de.removeSentPingLocked(m.TxID, sp)
|
||||
|
||||
st, ok := de.endpointState[sp.to]
|
||||
if !ok {
|
||||
// This is no longer an endpoint we care about.
|
||||
return
|
||||
}
|
||||
|
||||
de.c.setAddrToDiscoLocked(src, de.discoKey, de)
|
||||
|
||||
now := time.Now()
|
||||
latency := now.Sub(sp.at)
|
||||
|
||||
if !isDerp {
|
||||
st, ok := de.endpointState[sp.to]
|
||||
if !ok {
|
||||
// This is no longer an endpoint we care about.
|
||||
return
|
||||
}
|
||||
|
||||
de.c.setAddrToDiscoLocked(src, de.discoKey, de)
|
||||
|
||||
st.addPongReplyLocked(pongReply{
|
||||
latency: latency,
|
||||
pongAt: now,
|
||||
from: src,
|
||||
pongSrc: m.Src,
|
||||
})
|
||||
}
|
||||
st.addPongReplyLocked(pongReply{
|
||||
latency: latency,
|
||||
pongAt: now,
|
||||
from: src,
|
||||
pongSrc: m.Src,
|
||||
})
|
||||
|
||||
if sp.purpose != pingHeartbeat {
|
||||
de.c.logf("magicsock: disco: %v<-%v (%v, %v) got pong tx=%x latency=%v pong.src=%v%v", de.c.discoShort, de.discoShort, de.publicKey.ShortString(), src, m.TxID[:6], latency.Round(time.Millisecond), m.Src, logger.ArgWriter(func(bw *bufio.Writer) {
|
||||
@@ -3674,27 +3427,19 @@ func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort)
|
||||
}))
|
||||
}
|
||||
|
||||
for _, pp := range de.pendingCLIPings {
|
||||
de.c.populateCLIPingResponseLocked(pp.res, latency, sp.to)
|
||||
go pp.cb(pp.res)
|
||||
}
|
||||
de.pendingCLIPings = nil
|
||||
|
||||
// Promote this pong response to our current best address if it's lower latency.
|
||||
// TODO(bradfitz): decide how latency vs. preference order affects decision
|
||||
if !isDerp {
|
||||
if de.bestAddr.IsZero() || latency < de.bestAddrLatency {
|
||||
if de.bestAddr != sp.to {
|
||||
de.c.logf("magicsock: disco: node %v %v now using %v", de.publicKey.ShortString(), de.discoShort, sp.to)
|
||||
de.bestAddr = sp.to
|
||||
}
|
||||
}
|
||||
if de.bestAddr == sp.to {
|
||||
de.bestAddrLatency = latency
|
||||
de.bestAddrAt = now
|
||||
de.trustBestAddrUntil = now.Add(trustUDPAddrDuration)
|
||||
if de.bestAddr.IsZero() || latency < de.bestAddrLatency {
|
||||
if de.bestAddr != sp.to {
|
||||
de.c.logf("magicsock: disco: node %v %v now using %v", de.publicKey.ShortString(), de.discoShort, sp.to)
|
||||
de.bestAddr = sp.to
|
||||
}
|
||||
}
|
||||
if de.bestAddr == sp.to {
|
||||
de.bestAddrLatency = latency
|
||||
de.bestAddrAt = now
|
||||
de.trustBestAddrUntil = now.Add(trustUDPAddrDuration)
|
||||
}
|
||||
}
|
||||
|
||||
// discoEndpoint.mu must be held.
|
||||
@@ -3772,7 +3517,6 @@ func (de *discoEndpoint) stopAndReset() {
|
||||
de.heartBeatTimer.Stop()
|
||||
de.heartBeatTimer = nil
|
||||
}
|
||||
de.pendingCLIPings = nil
|
||||
}
|
||||
|
||||
// ippCache is a cache of *net.UDPAddr => netaddr.IPPort mappings.
|
||||
|
||||
@@ -110,7 +110,7 @@ func (m *Mon) pump() {
|
||||
default:
|
||||
}
|
||||
// Keep retrying while we're not closed.
|
||||
m.logf("error from link monitor: %v", err)
|
||||
m.logf("error receiving from connection: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ func newOSMon(logf logger.Logf) (osMon, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("devd dial error: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing devd socket: %v", err)
|
||||
}
|
||||
return &devdConn{conn}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -87,39 +87,28 @@ func (c *nlConn) Receive() (message, error) {
|
||||
Addr: netaddrIP(rmsg.Attributes.Local),
|
||||
Delete: msg.Header.Type == unix.RTM_DELADDR,
|
||||
}, nil
|
||||
case unix.RTM_NEWROUTE, unix.RTM_DELROUTE:
|
||||
typeStr := "RTM_NEWROUTE"
|
||||
if msg.Header.Type == unix.RTM_DELROUTE {
|
||||
typeStr = "RTM_DELROUTE"
|
||||
}
|
||||
case unix.RTM_NEWROUTE:
|
||||
var rmsg rtnetlink.RouteMessage
|
||||
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
|
||||
c.logf("%s: failed to parse: %v", typeStr, err)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
src := netaddrIPPrefix(rmsg.Attributes.Src, rmsg.SrcLength)
|
||||
dst := netaddrIPPrefix(rmsg.Attributes.Dst, rmsg.DstLength)
|
||||
gw := netaddrIP(rmsg.Attributes.Gateway)
|
||||
|
||||
if msg.Header.Type == unix.RTM_NEWROUTE && rmsg.Table == tsTable && rmsg.DstLength == 32 {
|
||||
// Don't log. Spammy and normal to see a bunch of these on start-up,
|
||||
// which we make ourselves.
|
||||
} else {
|
||||
c.logf("%s: src=%v, dst=%v, gw=%v, outif=%v, table=%v", typeStr,
|
||||
condNetAddrPrefix(src), condNetAddrPrefix(dst), condNetAddrIP(gw),
|
||||
rmsg.Attributes.OutIface, rmsg.Attributes.Table)
|
||||
}
|
||||
if msg.Header.Type == unix.RTM_DELROUTE {
|
||||
// Just logging it for now.
|
||||
// (Debugging https://github.com/tailscale/tailscale/issues/643)
|
||||
c.logf("RTM_NEWROUTE: failed to parse: %v", err)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
return &newRouteMessage{
|
||||
Table: rmsg.Table,
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Gateway: gw,
|
||||
Src: netaddrIP(rmsg.Attributes.Src),
|
||||
Dst: netaddrIP(rmsg.Attributes.Dst),
|
||||
Gateway: netaddrIP(rmsg.Attributes.Gateway),
|
||||
}, nil
|
||||
case unix.RTM_DELROUTE:
|
||||
var rmsg rtnetlink.RouteMessage
|
||||
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
|
||||
c.logf("RTM_DELROUTE: failed to parse: %v", err)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
// Just log it for now, but don't bubble it up.
|
||||
// (Debugging https://github.com/tailscale/tailscale/issues/643)
|
||||
c.logf("RTM_DELROUTE: %+v", rmsg)
|
||||
return unspecifiedMessage{}, nil
|
||||
default:
|
||||
c.logf("unhandled netlink msg type %+v, %q", msg.Header, msg.Data)
|
||||
return unspecifiedMessage{}, nil
|
||||
@@ -131,36 +120,14 @@ func netaddrIP(std net.IP) netaddr.IP {
|
||||
return ip
|
||||
}
|
||||
|
||||
func netaddrIPPrefix(std net.IP, bits uint8) netaddr.IPPrefix {
|
||||
ip, _ := netaddr.FromStdIP(std)
|
||||
return netaddr.IPPrefix{IP: ip, Bits: bits}
|
||||
}
|
||||
|
||||
func condNetAddrPrefix(ipp netaddr.IPPrefix) string {
|
||||
if ipp.IP.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ipp.String()
|
||||
}
|
||||
|
||||
func condNetAddrIP(ip netaddr.IP) string {
|
||||
if ip.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// newRouteMessage is a message for a new route being added.
|
||||
type newRouteMessage struct {
|
||||
Src, Dst netaddr.IPPrefix
|
||||
Gateway netaddr.IP
|
||||
Table uint8
|
||||
Src, Dst, Gateway netaddr.IP
|
||||
Table uint8
|
||||
}
|
||||
|
||||
const tsTable = 52
|
||||
|
||||
func (m *newRouteMessage) ignore() bool {
|
||||
return m.Table == tsTable || tsaddr.IsTailscaleIP(m.Dst.IP)
|
||||
return m.Table == 88 || tsaddr.IsTailscaleIP(m.Dst)
|
||||
}
|
||||
|
||||
// newAddrMessage is a message for a new address being added.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!freebsd,!windows android
|
||||
// +build !linux,!freebsd android
|
||||
|
||||
package monitor
|
||||
|
||||
|
||||
@@ -1,212 +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.
|
||||
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
pollIntervalSlow = 15 * time.Second
|
||||
pollIntervalFast = 3 * time.Second
|
||||
pollFastFor = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
|
||||
notifyAddrChangeProc = iphlpapi.NewProc("NotifyAddrChange")
|
||||
notifyRouteChangeProc = iphlpapi.NewProc("NotifyRouteChange")
|
||||
)
|
||||
|
||||
type unspecifiedMessage struct{}
|
||||
|
||||
func (unspecifiedMessage) ignore() bool { return false }
|
||||
|
||||
type pollStateChangedMessage struct{}
|
||||
|
||||
func (pollStateChangedMessage) ignore() bool { return false }
|
||||
|
||||
type messageOrError struct {
|
||||
message
|
||||
error
|
||||
}
|
||||
|
||||
type winMon struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
messagec chan messageOrError
|
||||
logf logger.Logf
|
||||
pollTicker *time.Ticker
|
||||
lastState *interfaces.State
|
||||
|
||||
mu sync.Mutex
|
||||
event windows.Handle
|
||||
lastNetChange time.Time
|
||||
inFastPoll bool // recent net change event made us go into fast polling mode (to detect proxy changes)
|
||||
}
|
||||
|
||||
func newOSMon(logf logger.Logf) (osMon, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &winMon{
|
||||
logf: logf,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
messagec: make(chan messageOrError, 1),
|
||||
pollTicker: time.NewTicker(pollIntervalSlow),
|
||||
}
|
||||
go m.awaitIPAndRouteChanges()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *winMon) Close() error {
|
||||
m.cancel()
|
||||
m.pollTicker.Stop()
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if h := m.event; h != 0 {
|
||||
// Wake up any reader blocked in Receive.
|
||||
windows.SetEvent(h)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var errClosed = errors.New("closed")
|
||||
|
||||
func (m *winMon) Receive() (message, error) {
|
||||
for {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return nil, errClosed
|
||||
case me := <-m.messagec:
|
||||
return me.message, me.error
|
||||
case <-m.pollTicker.C:
|
||||
if m.stateChanged() {
|
||||
m.logf("interface state changed (on poll)")
|
||||
return pollStateChangedMessage{}, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
if m.inFastPoll && time.Since(m.lastNetChange) > pollFastFor {
|
||||
m.inFastPoll = false
|
||||
m.pollTicker.Reset(pollIntervalSlow)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *winMon) stateChanged() bool {
|
||||
st, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
changed := !st.Equal(m.lastState)
|
||||
m.lastState = st
|
||||
return changed
|
||||
}
|
||||
|
||||
func (m *winMon) awaitIPAndRouteChanges() {
|
||||
for {
|
||||
msg, err := m.getIPOrRouteChangeMessage()
|
||||
if err == errClosed {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case m.messagec <- messageOrError{msg, err}:
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *winMon) getIPOrRouteChangeMessage() (message, error) {
|
||||
if m.ctx.Err() != nil {
|
||||
return nil, errClosed
|
||||
}
|
||||
|
||||
var o windows.Overlapped
|
||||
h, err := windows.CreateEvent(nil, 1 /* true*/, 0 /* unsignaled */, nil /* no name */)
|
||||
if err != nil {
|
||||
m.logf("CreateEvent: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CloseHandle(h)
|
||||
|
||||
m.mu.Lock()
|
||||
m.event = h
|
||||
m.mu.Unlock()
|
||||
|
||||
o.HEvent = h
|
||||
|
||||
err = notifyAddrChange(&h, &o)
|
||||
if err != nil {
|
||||
m.logf("notifyAddrChange: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
err = notifyRouteChange(&h, &o)
|
||||
if err != nil {
|
||||
m.logf("notifyRouteChange: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
_, err = windows.WaitForSingleObject(o.HEvent, windows.INFINITE)
|
||||
if m.ctx.Err() != nil {
|
||||
return nil, errClosed
|
||||
}
|
||||
if err != nil {
|
||||
m.logf("waitForSingleObject: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
d := time.Since(t0)
|
||||
m.logf("got windows change event after %v", d)
|
||||
|
||||
m.mu.Lock()
|
||||
{
|
||||
m.lastNetChange = time.Now()
|
||||
m.event = 0
|
||||
|
||||
// Something changed, so assume Windows is about to
|
||||
// discover its new proxy settings from WPAD, which
|
||||
// seems to take a bit. Poll heavily for awhile.
|
||||
m.logf("starting quick poll, waiting for WPAD change")
|
||||
m.inFastPoll = true
|
||||
m.pollTicker.Reset(pollIntervalFast)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
|
||||
func notifyAddrChange(h *windows.Handle, o *windows.Overlapped) error {
|
||||
return callNotifyProc(notifyAddrChangeProc, h, o)
|
||||
}
|
||||
|
||||
func notifyRouteChange(h *windows.Handle, o *windows.Overlapped) error {
|
||||
return callNotifyProc(notifyAddrChangeProc, h, o)
|
||||
}
|
||||
|
||||
func callNotifyProc(p *syscall.LazyProc, h *windows.Handle, o *windows.Overlapped) error {
|
||||
r1, _, e1 := p.Call(uintptr(unsafe.Pointer(h)), uintptr(unsafe.Pointer(o)))
|
||||
expect := uintptr(0)
|
||||
if h != nil || o != nil {
|
||||
const ERROR_IO_PENDING = 997
|
||||
expect = ERROR_IO_PENDING
|
||||
}
|
||||
if r1 == expect {
|
||||
return nil
|
||||
}
|
||||
return e1
|
||||
}
|
||||
@@ -23,7 +23,6 @@ type Config struct {
|
||||
// if the manager does not support per-domain settings.
|
||||
PerDomain bool
|
||||
// Proxied indicates whether DNS requests are proxied through a tsdns.Resolver.
|
||||
// This enables Magic DNS.
|
||||
Proxied bool
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// This is particularly useful because certain conditions can cause indefinite hangs
|
||||
// (such as improper dbus auth followed by contextless dbus.Object.Call).
|
||||
// Such operations should be wrapped in a timeout context.
|
||||
const reconfigTimeout = time.Second //lint:ignore U1000 used on Linux at least, maybe others later
|
||||
const reconfigTimeout = time.Second
|
||||
|
||||
type managerImpl interface {
|
||||
// Up updates system DNS settings to match the given configuration.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -14,12 +13,6 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
||||
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
|
||||
tsRegBase = `SOFTWARE\Tailscale IPN`
|
||||
)
|
||||
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
@@ -32,88 +25,29 @@ func newManager(mconfig ManagerConfig) managerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func setRegistryString(path, name, value string) error {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", path, err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
err = key.SetStringValue(name, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting %s[%s]: %w", path, name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRegistryString(path, name string) (string, error) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("opening %s: %w", path, err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
value, _, err := key.GetStringValue(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting %s[%s]: %w", path, name, err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
|
||||
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
value := strings.Join(nameservers, ",")
|
||||
return setRegistryString(path, "NameServer", value)
|
||||
}
|
||||
|
||||
func (m windowsManager) setDomains(path string, oldDomains, newDomains []string) error {
|
||||
// We reimplement setRegistryString to ensure that we hold the key for the whole operation.
|
||||
func setRegistry(path, nameservers, domains string) error {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ|registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", path, err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
searchList, _, err := key.GetStringValue("SearchList")
|
||||
if err != nil && err != registry.ErrNotExist {
|
||||
return fmt.Errorf("getting %s[SearchList]: %w", path, err)
|
||||
err = key.SetStringValue("NameServer", nameservers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting %s/NameServer: %w", path, err)
|
||||
}
|
||||
currentDomains := strings.Split(searchList, ",")
|
||||
|
||||
var domainsToSet []string
|
||||
for _, domain := range currentDomains {
|
||||
inOld, inNew := false, false
|
||||
|
||||
// The number of domains should be small,
|
||||
// so this is probaly faster than constructing a map.
|
||||
for _, oldDomain := range oldDomains {
|
||||
if domain == oldDomain {
|
||||
inOld = true
|
||||
}
|
||||
}
|
||||
for _, newDomain := range newDomains {
|
||||
if domain == newDomain {
|
||||
inNew = true
|
||||
}
|
||||
}
|
||||
|
||||
if !inNew && !inOld {
|
||||
domainsToSet = append(domainsToSet, domain)
|
||||
}
|
||||
err = key.SetStringValue("Domain", domains)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting %s/Domain: %w", path, err)
|
||||
}
|
||||
domainsToSet = append(domainsToSet, newDomains...)
|
||||
|
||||
searchList = strings.Join(domainsToSet, ",")
|
||||
if err := key.SetStringValue("SearchList", searchList); err != nil {
|
||||
return fmt.Errorf("setting %s[SearchList]: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) Up(config Config) error {
|
||||
var ipsv4 []string
|
||||
var ipsv6 []string
|
||||
|
||||
for _, ip := range config.Nameservers {
|
||||
if ip.Is4() {
|
||||
ipsv4 = append(ipsv4, ip.String())
|
||||
@@ -121,29 +55,23 @@ func (m windowsManager) Up(config Config) error {
|
||||
ipsv6 = append(ipsv6, ip.String())
|
||||
}
|
||||
}
|
||||
nsv4 := strings.Join(ipsv4, ",")
|
||||
nsv6 := strings.Join(ipsv6, ",")
|
||||
|
||||
lastSearchList, err := getRegistryString(tsRegBase, "SearchList")
|
||||
if err != nil && !errors.Is(err, registry.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
lastDomains := strings.Split(lastSearchList, ",")
|
||||
|
||||
if err := m.setNameservers(ipv4RegBase, ipsv4); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.setDomains(ipv4RegBase, lastDomains, config.Domains); err != nil {
|
||||
return err
|
||||
var domains string
|
||||
if len(config.Domains) > 0 {
|
||||
if len(config.Domains) > 1 {
|
||||
m.logf("only a single search domain is supported")
|
||||
}
|
||||
domains = config.Domains[0]
|
||||
}
|
||||
|
||||
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil {
|
||||
v4Path := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + m.guid
|
||||
if err := setRegistry(v4Path, nsv4, domains); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.setDomains(ipv6RegBase, lastDomains, config.Domains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newSearchList := strings.Join(config.Domains, ",")
|
||||
if err := setRegistryString(tsRegBase, "SearchList", newSearchList); err != nil {
|
||||
v6Path := `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\` + m.guid
|
||||
if err := setRegistry(v6Path, nsv6, domains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -14,25 +14,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"unsafe"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
var nativeEndian binary.ByteOrder
|
||||
|
||||
func init() {
|
||||
// TODO(dmytro): use DBus endianness flag when available.
|
||||
// A more elegant way to do this is by looking at the first byte of a raw DBus message.
|
||||
// However, that requires a change in godbus, which has slow maintainer response.
|
||||
i := uint32(1)
|
||||
p := unsafe.Pointer(&i)
|
||||
if *(*byte)(p) == 1 {
|
||||
nativeEndian = binary.LittleEndian
|
||||
} else {
|
||||
nativeEndian = binary.BigEndian
|
||||
}
|
||||
}
|
||||
type nmConnectionSettings map[string]map[string]dbus.Variant
|
||||
|
||||
// isNMActive determines if NetworkManager is currently managing system DNS settings.
|
||||
func isNMActive() bool {
|
||||
@@ -75,8 +61,6 @@ func newNMManager(mconfig ManagerConfig) managerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
type nmConnectionSettings map[string]map[string]dbus.Variant
|
||||
|
||||
// Up implements managerImpl.
|
||||
func (m nmManager) Up(config Config) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
@@ -145,7 +129,7 @@ func (m nmManager) Up(config Config) error {
|
||||
for _, ip := range config.Nameservers {
|
||||
b := ip.As16()
|
||||
if ip.Is4() {
|
||||
dnsv4 = append(dnsv4, nativeEndian.Uint32(b[12:]))
|
||||
dnsv4 = append(dnsv4, binary.LittleEndian.Uint32(b[12:]))
|
||||
} else {
|
||||
dnsv6 = append(dnsv6, b[:])
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func bindSocketRoute(family winipcfg.AddressFamily, device *device.Device, ourLu
|
||||
return bind.BindSocketToInterface6(index, false)
|
||||
}
|
||||
} else {
|
||||
log.Printf("WARNING: skipping windows socket binding.")
|
||||
log.Printf("WARNING: skipping windows socket binding.\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -194,10 +194,10 @@ func setFirewall(ifcGUID *windows.GUID) (bool, error) {
|
||||
return false, fmt.Errorf("nco.GetAdapterId: %v", err)
|
||||
}
|
||||
if aid != ifcGUID.String() {
|
||||
log.Printf("skipping adapter id: %v", aid)
|
||||
log.Printf("skipping adapter id: %v\n", aid)
|
||||
continue
|
||||
}
|
||||
log.Printf("found! adapter id: %v", aid)
|
||||
log.Printf("found! adapter id: %v\n", aid)
|
||||
|
||||
n, err := nco.GetNetwork()
|
||||
if err != nil {
|
||||
@@ -216,7 +216,7 @@ func setFirewall(ifcGUID *windows.GUID) (bool, error) {
|
||||
return false, fmt.Errorf("SetCategory: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("setFirewall: already category %v", cat)
|
||||
log.Printf("setFirewall: already category %v\n", cat)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -228,7 +228,7 @@ func setFirewall(ifcGUID *windows.GUID) (bool, error) {
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
const mtu = 0
|
||||
guid := tun.GUID()
|
||||
log.Printf("wintun GUID is %v", guid)
|
||||
log.Printf("wintun GUID is %v\n", guid)
|
||||
iface, err := winipcfg.InterfaceFromGUID(&guid)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -241,7 +241,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
for i := 0; i < 20; i++ {
|
||||
found, err := setFirewall(&guid)
|
||||
if err != nil {
|
||||
log.Printf("setFirewall: %v", err)
|
||||
log.Printf("setFirewall: %v\n", err)
|
||||
// fall through anyway, this isn't fatal.
|
||||
}
|
||||
if found {
|
||||
@@ -315,7 +315,15 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(routes, func(i, j int) bool { return routeLess(&routes[i], &routes[j]) })
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return (bytes.Compare(routes[i].Destination.IP, routes[j].Destination.IP) == -1 ||
|
||||
// Narrower masks first
|
||||
bytes.Compare(routes[i].Destination.Mask, routes[j].Destination.Mask) == 1 ||
|
||||
// No nexthop before non-empty nexthop
|
||||
bytes.Compare(routes[i].NextHop, routes[j].NextHop) == -1 ||
|
||||
// Lower metrics first
|
||||
routes[i].Metric < routes[j].Metric)
|
||||
})
|
||||
|
||||
deduplicatedRoutes := []*winipcfg.RouteData{}
|
||||
for i := 0; i < len(routes); i++ {
|
||||
@@ -328,21 +336,21 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
}
|
||||
deduplicatedRoutes = append(deduplicatedRoutes, &routes[i])
|
||||
}
|
||||
log.Printf("routes: %v", routes)
|
||||
log.Printf("routes: %v\n", routes)
|
||||
|
||||
var errAcc error
|
||||
err = iface.SyncRoutes(deduplicatedRoutes)
|
||||
if err != nil && errAcc == nil {
|
||||
log.Printf("setroutes: %v", err)
|
||||
log.Printf("setroutes: %v\n", err)
|
||||
errAcc = err
|
||||
}
|
||||
|
||||
ipif, err := iface.GetIpInterface(winipcfg.AF_INET)
|
||||
if err != nil {
|
||||
log.Printf("getipif: %v", err)
|
||||
log.Printf("getipif: %v\n", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("foundDefault4: %v", foundDefault4)
|
||||
log.Printf("foundDefault4: %v\n", foundDefault4)
|
||||
if foundDefault4 {
|
||||
ipif.UseAutomaticMetric = false
|
||||
ipif.Metric = 0
|
||||
@@ -379,25 +387,3 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
|
||||
return errAcc
|
||||
}
|
||||
|
||||
// routeLess reports whether ri should sort before rj.
|
||||
// The actual sort order doesn't appear to matter. The caller just
|
||||
// wants them sorted to be able to de-dup.
|
||||
func routeLess(ri, rj *winipcfg.RouteData) bool {
|
||||
if v := bytes.Compare(ri.Destination.IP, rj.Destination.IP); v != 0 {
|
||||
return v == -1
|
||||
}
|
||||
if v := bytes.Compare(ri.Destination.Mask, rj.Destination.Mask); v != 0 {
|
||||
// Narrower masks first
|
||||
return v == 1
|
||||
}
|
||||
if ri.Metric != rj.Metric {
|
||||
// Lower metrics first
|
||||
return ri.Metric < rj.Metric
|
||||
}
|
||||
if v := bytes.Compare(ri.NextHop, rj.NextHop); v != 0 {
|
||||
// No nexthop before non-empty nexthop.
|
||||
return v == -1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,97 +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.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
winipcfg "github.com/tailscale/winipcfg-go"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func randIP() net.IP {
|
||||
b := byte(rand.Intn(3))
|
||||
return net.IP{b, b, b, b}
|
||||
}
|
||||
|
||||
func randRouteData() *winipcfg.RouteData {
|
||||
return &winipcfg.RouteData{
|
||||
Destination: net.IPNet{
|
||||
IP: randIP(),
|
||||
Mask: net.CIDRMask(rand.Intn(3)+1, 32),
|
||||
},
|
||||
NextHop: randIP(),
|
||||
Metric: uint32(rand.Intn(3)),
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteLess(t *testing.T) {
|
||||
type D = winipcfg.RouteData
|
||||
ipnet := func(s string) net.IPNet {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing test data %q: %v", s, err)
|
||||
}
|
||||
return *ipp.IPNet()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ri, rj *winipcfg.RouteData
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
ri: &D{Metric: 1},
|
||||
rj: &D{Metric: 2},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
ri: &D{Destination: ipnet("1.1.0.0/16"), Metric: 2},
|
||||
rj: &D{Destination: ipnet("2.2.0.0/16"), Metric: 1},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
ri: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1},
|
||||
rj: &D{Destination: ipnet("2.2.0.0/16"), Metric: 1},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
ri: &D{Destination: ipnet("1.1.0.0/32"), Metric: 2},
|
||||
rj: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
ri: &D{Destination: ipnet("1.1.0.0/32"), Metric: 1},
|
||||
rj: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
ri: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1, NextHop: net.ParseIP("3.3.3.3")},
|
||||
rj: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1, NextHop: net.ParseIP("4.4.4.4")},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := routeLess(tt.ri, tt.rj)
|
||||
if got != tt.want {
|
||||
t.Errorf("%v. less = %v; want %v", i, got, tt.want)
|
||||
}
|
||||
back := routeLess(tt.rj, tt.ri)
|
||||
if back && got {
|
||||
t.Errorf("%v. less both ways", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteLessConsistent(t *testing.T) {
|
||||
for i := 0; i < 10000; i++ {
|
||||
ri := randRouteData()
|
||||
rj := randRouteData()
|
||||
if routeLess(ri, rj) && routeLess(rj, ri) {
|
||||
t.Fatalf("both compare less to each other:\n\t%#v\nand\n\t%#v", ri, rj)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (
|
||||
|
||||
func cmd(args ...string) *exec.Cmd {
|
||||
if len(args) == 0 {
|
||||
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args)
|
||||
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]\n", args)
|
||||
}
|
||||
return exec.Command(args[0], args[1:]...)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
)
|
||||
|
||||
@@ -49,7 +48,7 @@ func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device
|
||||
|
||||
func cmd(args ...string) *exec.Cmd {
|
||||
if len(args) == 0 {
|
||||
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args)
|
||||
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]\n", args)
|
||||
}
|
||||
return exec.Command(args[0], args[1:]...)
|
||||
}
|
||||
@@ -115,12 +114,8 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
|
||||
net := route.IPNet()
|
||||
nip := net.IP.Mask(net.Mask)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits)
|
||||
del := "del"
|
||||
if version.OS() == "macOS" {
|
||||
del = "delete"
|
||||
}
|
||||
routedel := []string{"route", "-q", "-n",
|
||||
del, "-inet", nstr,
|
||||
"del", "-inet", nstr,
|
||||
"-iface", r.tunname}
|
||||
out, err := cmd(routedel...).CombinedOutput()
|
||||
if err != nil {
|
||||
|
||||
@@ -52,7 +52,7 @@ func (r *winRouter) Up() error {
|
||||
var err error
|
||||
r.routeChangeCallback, err = monitorDefaultRoutes(r.wgdev, true, r.nativeTun)
|
||||
if err != nil {
|
||||
log.Fatalf("MonitorDefaultRoutes: %v", err)
|
||||
log.Fatalf("MonitorDefaultRoutes: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (r *winRouter) Set(cfg *Config) error {
|
||||
|
||||
err := configureInterface(cfg, r.nativeTun)
|
||||
if err != nil {
|
||||
r.logf("ConfigureInterface: %v", err)
|
||||
r.logf("ConfigureInterface: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,287 +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.
|
||||
|
||||
package tsdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// headerBytes is the number of bytes in a DNS message header.
|
||||
const headerBytes = 12
|
||||
|
||||
// connCount is the number of UDP connections to use for forwarding.
|
||||
const connCount = 32
|
||||
|
||||
const (
|
||||
// cleanupInterval is the interval between purged of timed-out entries from txMap.
|
||||
cleanupInterval = 30 * time.Second
|
||||
// responseTimeout is the maximal amount of time to wait for a DNS response.
|
||||
responseTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var errNoUpstreams = errors.New("upstream nameservers not set")
|
||||
|
||||
var aLongTimeAgo = time.Unix(0, 1)
|
||||
|
||||
type forwardingRecord struct {
|
||||
src netaddr.IPPort
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
// txid identifies a DNS transaction.
|
||||
//
|
||||
// As the standard DNS Request ID is only 16 bits, we extend it:
|
||||
// the lower 32 bits are the zero-extended bits of the DNS Request ID;
|
||||
// the upper 32 bits are the CRC32 checksum of the first question in the request.
|
||||
// This makes probability of txid collision negligible.
|
||||
type txid uint64
|
||||
|
||||
// getTxID computes the txid of the given DNS packet.
|
||||
func getTxID(packet []byte) txid {
|
||||
if len(packet) < headerBytes {
|
||||
return 0
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// forwarder forwards DNS packets to a number of upstream nameservers.
|
||||
type forwarder struct {
|
||||
logf logger.Logf
|
||||
|
||||
// responses is a channel by which responses are returned.
|
||||
responses chan Packet
|
||||
// closed signals all goroutines to stop.
|
||||
closed chan struct{}
|
||||
// wg signals when all goroutines have stopped.
|
||||
wg sync.WaitGroup
|
||||
|
||||
// conns are the UDP connections used for forwarding.
|
||||
// A random one is selected for each request, regardless of the target upstream.
|
||||
conns []*net.UDPConn
|
||||
|
||||
mu sync.Mutex
|
||||
// upstreams are the nameserver addresses that should be used for forwarding.
|
||||
upstreams []net.Addr
|
||||
// txMap maps DNS txids to active forwarding records.
|
||||
txMap map[txid]forwardingRecord
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, responses chan Packet) *forwarder {
|
||||
return &forwarder{
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
responses: responses,
|
||||
closed: make(chan struct{}),
|
||||
conns: make([]*net.UDPConn, connCount),
|
||||
txMap: make(map[txid]forwardingRecord),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *forwarder) Start() error {
|
||||
var err error
|
||||
|
||||
for i := range f.conns {
|
||||
f.conns[i], err = net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
f.wg.Add(connCount + 1)
|
||||
for idx, conn := range f.conns {
|
||||
go f.recv(uint16(idx), conn)
|
||||
}
|
||||
go f.cleanMap()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *forwarder) Close() {
|
||||
select {
|
||||
case <-f.closed:
|
||||
return
|
||||
default:
|
||||
// continue
|
||||
}
|
||||
close(f.closed)
|
||||
|
||||
for _, conn := range f.conns {
|
||||
conn.SetDeadline(aLongTimeAgo)
|
||||
}
|
||||
|
||||
f.wg.Wait()
|
||||
|
||||
for _, conn := range f.conns {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *forwarder) setUpstreams(upstreams []net.Addr) {
|
||||
f.mu.Lock()
|
||||
f.upstreams = upstreams
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *forwarder) send(packet []byte, dst net.Addr) {
|
||||
connIdx := rand.Intn(connCount)
|
||||
conn := f.conns[connIdx]
|
||||
_, err := conn.WriteTo(packet, dst)
|
||||
// Do not log errors due to expired deadline.
|
||||
if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
f.logf("send: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *forwarder) recv(connIdx uint16, conn *net.UDPConn) {
|
||||
defer f.wg.Done()
|
||||
|
||||
for {
|
||||
out := make([]byte, maxResponseBytes)
|
||||
n, err := conn.Read(out)
|
||||
|
||||
if err != nil {
|
||||
// Do not log errors due to expired deadline.
|
||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
f.logf("recv: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if n < headerBytes {
|
||||
f.logf("recv: packet too small (%d bytes)", n)
|
||||
}
|
||||
|
||||
out = out[:n]
|
||||
txid := getTxID(out)
|
||||
|
||||
f.mu.Lock()
|
||||
|
||||
record, found := f.txMap[txid]
|
||||
// At most one nameserver will return a response:
|
||||
// the first one to do so will delete txid from the map.
|
||||
if !found {
|
||||
f.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
delete(f.txMap, txid)
|
||||
|
||||
f.mu.Unlock()
|
||||
|
||||
packet := Packet{
|
||||
Payload: out,
|
||||
Addr: record.src,
|
||||
}
|
||||
select {
|
||||
case <-f.closed:
|
||||
return
|
||||
case f.responses <- packet:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanMap periodically deletes timed-out forwarding records from f.txMap to bound growth.
|
||||
func (f *forwarder) cleanMap() {
|
||||
defer f.wg.Done()
|
||||
|
||||
t := time.NewTicker(cleanupInterval)
|
||||
defer t.Stop()
|
||||
|
||||
var now time.Time
|
||||
for {
|
||||
select {
|
||||
case <-f.closed:
|
||||
return
|
||||
case now = <-t.C:
|
||||
// continue
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
for k, v := range f.txMap {
|
||||
if now.Sub(v.createdAt) > responseTimeout {
|
||||
delete(f.txMap, k)
|
||||
}
|
||||
}
|
||||
f.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// forward forwards the query to all upstream nameservers and returns the first response.
|
||||
func (f *forwarder) forward(query Packet) error {
|
||||
txid := getTxID(query.Payload)
|
||||
|
||||
f.mu.Lock()
|
||||
|
||||
upstreams := f.upstreams
|
||||
if len(upstreams) == 0 {
|
||||
f.mu.Unlock()
|
||||
return errNoUpstreams
|
||||
}
|
||||
f.txMap[txid] = forwardingRecord{
|
||||
src: query.Addr,
|
||||
createdAt: time.Now(),
|
||||
}
|
||||
|
||||
f.mu.Unlock()
|
||||
|
||||
for _, upstream := range upstreams {
|
||||
f.send(query.Payload, upstream)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package tsdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -20,13 +21,10 @@ type Map struct {
|
||||
ipToName map[netaddr.IP]string
|
||||
// names are the keys of nameToIP in sorted order.
|
||||
names []string
|
||||
// rootDomains are the domains whose subdomains should always
|
||||
// be resolved locally to prevent leakage of sensitive names.
|
||||
rootDomains []string // e.g. "user.provider.beta.tailscale.net."
|
||||
}
|
||||
|
||||
// NewMap returns a new Map with name to address mapping given by nameToIP.
|
||||
func NewMap(initNameToIP map[string]netaddr.IP, rootDomains []string) *Map {
|
||||
func NewMap(initNameToIP map[string]netaddr.IP) *Map {
|
||||
// TODO(dmytro): we have to allocate names and ipToName, but nameToIP can be avoided.
|
||||
// It is here because control sends us names not in canonical form. Change this.
|
||||
names := make([]string, 0, len(initNameToIP))
|
||||
@@ -51,16 +49,12 @@ func NewMap(initNameToIP map[string]netaddr.IP, rootDomains []string) *Map {
|
||||
nameToIP: nameToIP,
|
||||
ipToName: ipToName,
|
||||
names: names,
|
||||
|
||||
rootDomains: rootDomains,
|
||||
}
|
||||
}
|
||||
|
||||
func printSingleNameIP(buf *strings.Builder, name string, ip netaddr.IP) {
|
||||
buf.WriteString(name)
|
||||
buf.WriteByte('\t')
|
||||
buf.WriteString(ip.String())
|
||||
buf.WriteByte('\n')
|
||||
// Output width is exactly 80 columns.
|
||||
fmt.Fprintf(buf, "%s\t%s\n", name, ip)
|
||||
}
|
||||
|
||||
func (m *Map) Pretty() string {
|
||||
|
||||
@@ -16,12 +16,12 @@ func TestPretty(t *testing.T) {
|
||||
dmap *Map
|
||||
want string
|
||||
}{
|
||||
{"empty", NewMap(nil, nil), ""},
|
||||
{"empty", NewMap(nil), ""},
|
||||
{
|
||||
"single",
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"hello.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
}, nil),
|
||||
}),
|
||||
"hello.ipn.dev.\t100.101.102.103\n",
|
||||
},
|
||||
{
|
||||
@@ -29,7 +29,7 @@ func TestPretty(t *testing.T) {
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.domain.": netaddr.IPv4(100, 101, 102, 103),
|
||||
"test2.sub.domain.": netaddr.IPv4(100, 99, 9, 1),
|
||||
}, nil),
|
||||
}),
|
||||
"test1.domain.\t100.101.102.103\ntest2.sub.domain.\t100.99.9.1\n",
|
||||
},
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
}, nil),
|
||||
}),
|
||||
"+test1.ipn.dev.\t100.101.102.103\n+test2.ipn.dev.\t100.103.102.101\n",
|
||||
},
|
||||
{
|
||||
@@ -65,11 +65,11 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
}, nil),
|
||||
}),
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
}, nil),
|
||||
}),
|
||||
"",
|
||||
},
|
||||
{
|
||||
@@ -77,11 +77,11 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
}, nil),
|
||||
}),
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 104, 102, 101),
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
}, nil),
|
||||
}),
|
||||
"-test2.ipn.dev.\t100.103.102.101\n+test2.ipn.dev.\t100.104.102.101\n",
|
||||
},
|
||||
{
|
||||
@@ -89,12 +89,12 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
}, nil),
|
||||
}),
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test3.ipn.dev.": netaddr.IPv4(100, 105, 106, 107),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
}, nil),
|
||||
}),
|
||||
"+test3.ipn.dev.\t100.105.106.107\n",
|
||||
},
|
||||
{
|
||||
@@ -102,10 +102,10 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
}, nil),
|
||||
}),
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
|
||||
}, nil),
|
||||
}),
|
||||
"-test2.ipn.dev.\t100.103.102.101\n",
|
||||
},
|
||||
{
|
||||
@@ -115,12 +115,12 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
"test4.ipn.dev.": netaddr.IPv4(100, 107, 106, 105),
|
||||
"test5.ipn.dev.": netaddr.IPv4(100, 64, 1, 1),
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
|
||||
}, nil),
|
||||
}),
|
||||
NewMap(map[string]netaddr.IP{
|
||||
"test2.ipn.dev.": netaddr.IPv4(100, 104, 102, 101),
|
||||
"test1.ipn.dev.": netaddr.IPv4(100, 100, 101, 102),
|
||||
"test3.ipn.dev.": netaddr.IPv4(100, 64, 1, 1),
|
||||
}, nil),
|
||||
}),
|
||||
"-test1.ipn.dev.\t100.101.102.103\n+test1.ipn.dev.\t100.100.101.102\n" +
|
||||
"-test2.ipn.dev.\t100.103.102.101\n+test2.ipn.dev.\t100.104.102.101\n" +
|
||||
"+test3.ipn.dev.\t100.64.1.1\n-test4.ipn.dev.\t100.107.106.105\n-test5.ipn.dev.\t100.64.1.1\n",
|
||||
|
||||
@@ -7,25 +7,30 @@
|
||||
package tsdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// maxResponseBytes is the maximum size of a response from a Resolver.
|
||||
const maxResponseBytes = 512
|
||||
// maxResponseSize is the maximum size of a response from a Resolver.
|
||||
const maxResponseSize = 512
|
||||
|
||||
// queueSize is the maximal number of DNS requests that can await polling.
|
||||
// queueSize is the maximal number of DNS requests that can be pending at a time.
|
||||
// If EnqueueRequest is called when this many requests are already pending,
|
||||
// the request will be dropped to avoid blocking the caller.
|
||||
const queueSize = 64
|
||||
const queueSize = 8
|
||||
|
||||
// delegateTimeout is the maximal amount of time Resolver will wait
|
||||
// for upstream nameservers to process a query.
|
||||
const delegateTimeout = 5 * time.Second
|
||||
|
||||
// defaultTTL is the TTL of all responses from Resolver.
|
||||
const defaultTTL = 600 * time.Second
|
||||
@@ -34,12 +39,12 @@ const defaultTTL = 600 * time.Second
|
||||
var ErrClosed = errors.New("closed")
|
||||
|
||||
var (
|
||||
errAllFailed = errors.New("all upstream nameservers failed")
|
||||
errFullQueue = errors.New("request queue full")
|
||||
errNoNameservers = errors.New("no upstream nameservers set")
|
||||
errMapNotSet = errors.New("domain map not set")
|
||||
errNotForwarding = errors.New("forwarding disabled")
|
||||
errNotImplemented = errors.New("query type not implemented")
|
||||
errNotQuery = errors.New("not a DNS query")
|
||||
errNotOurName = errors.New("not a Tailscale DNS name")
|
||||
)
|
||||
|
||||
// Packet represents a DNS payload together with the address of its origin.
|
||||
@@ -58,64 +63,57 @@ type Packet struct {
|
||||
// it delegates to upstream nameservers if any are set.
|
||||
type Resolver struct {
|
||||
logf logger.Logf
|
||||
// forwarder forwards requests to upstream nameservers.
|
||||
forwarder *forwarder
|
||||
|
||||
// The asynchronous interface is due to the fact that resolution may potentially
|
||||
// block for a long time (if the upstream nameserver is slow to reach).
|
||||
|
||||
// queue is a buffered channel holding DNS requests queued for resolution.
|
||||
queue chan Packet
|
||||
// responses is an unbuffered channel to which responses are returned.
|
||||
// responses is an unbuffered channel to which responses are sent.
|
||||
responses chan Packet
|
||||
// errors is an unbuffered channel to which errors are returned.
|
||||
// errors is an unbuffered channel to which errors are sent.
|
||||
errors chan error
|
||||
// closed signals all goroutines to stop.
|
||||
// closed notifies the poll goroutines to stop.
|
||||
closed chan struct{}
|
||||
// wg signals when all goroutines have stopped.
|
||||
wg sync.WaitGroup
|
||||
// pollGroup signals when all poll goroutines have stopped.
|
||||
pollGroup sync.WaitGroup
|
||||
|
||||
// rootDomain is <root> in <mynode>.<mydomain>.<root>.
|
||||
rootDomain []byte
|
||||
|
||||
// dialer is the netns.Dialer used for delegation.
|
||||
dialer netns.Dialer
|
||||
|
||||
// mu guards the following fields from being updated while used.
|
||||
mu sync.Mutex
|
||||
// dnsMap is the map most recently received from the control server.
|
||||
dnsMap *Map
|
||||
}
|
||||
|
||||
// ResolverConfig is the set of configuration options for a Resolver.
|
||||
type ResolverConfig struct {
|
||||
// Logf is the logger to use throughout the Resolver.
|
||||
Logf logger.Logf
|
||||
// Forward determines whether the resolver will forward packets to
|
||||
// nameservers set with SetUpstreams if the domain name is not of a Tailscale node.
|
||||
Forward bool
|
||||
// nameservers is the list of nameserver addresses that should be used
|
||||
// if the received query is not for a Tailscale node.
|
||||
// The addresses are strings of the form ip:port, as expected by Dial.
|
||||
nameservers []string
|
||||
}
|
||||
|
||||
// NewResolver constructs a resolver associated with the given root domain.
|
||||
// The root domain must be in canonical form (with a trailing period).
|
||||
func NewResolver(config ResolverConfig) *Resolver {
|
||||
func NewResolver(logf logger.Logf, rootDomain string) *Resolver {
|
||||
r := &Resolver{
|
||||
logf: logger.WithPrefix(config.Logf, "tsdns: "),
|
||||
queue: make(chan Packet, queueSize),
|
||||
responses: make(chan Packet),
|
||||
errors: make(chan error),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
if config.Forward {
|
||||
r.forwarder = newForwarder(r.logf, r.responses)
|
||||
logf: logger.WithPrefix(logf, "tsdns: "),
|
||||
queue: make(chan Packet, queueSize),
|
||||
responses: make(chan Packet),
|
||||
errors: make(chan error),
|
||||
closed: make(chan struct{}),
|
||||
rootDomain: []byte(rootDomain),
|
||||
dialer: netns.NewDialer(),
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Resolver) Start() error {
|
||||
if r.forwarder != nil {
|
||||
if err := r.forwarder.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.wg.Add(1)
|
||||
func (r *Resolver) Start() {
|
||||
// TODO(dmytro): spawn more than one goroutine? They block on delegation.
|
||||
r.pollGroup.Add(1)
|
||||
go r.poll()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close shuts down the resolver and ensures poll goroutines have exited.
|
||||
@@ -128,12 +126,7 @@ func (r *Resolver) Close() {
|
||||
// continue
|
||||
}
|
||||
close(r.closed)
|
||||
|
||||
if r.forwarder != nil {
|
||||
r.forwarder.Close()
|
||||
}
|
||||
|
||||
r.wg.Wait()
|
||||
r.pollGroup.Wait()
|
||||
}
|
||||
|
||||
// SetMap sets the resolver's DNS map, taking ownership of it.
|
||||
@@ -145,13 +138,14 @@ func (r *Resolver) SetMap(m *Map) {
|
||||
r.logf("map diff:\n%s", m.PrettyDiffFrom(oldMap))
|
||||
}
|
||||
|
||||
// SetUpstreams sets the addresses of the resolver's
|
||||
// SetUpstreamNameservers sets the addresses of the resolver's
|
||||
// upstream nameservers, taking ownership of the argument.
|
||||
func (r *Resolver) SetUpstreams(upstreams []net.Addr) {
|
||||
if r.forwarder != nil {
|
||||
r.forwarder.setUpstreams(upstreams)
|
||||
}
|
||||
r.logf("set upstreams: %v", upstreams)
|
||||
// The addresses should be strings of the form ip:port,
|
||||
// matching what Dial("udp", addr) expects as addr.
|
||||
func (r *Resolver) SetNameservers(nameservers []string) {
|
||||
r.mu.Lock()
|
||||
r.nameservers = nameservers
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// EnqueueRequest places the given DNS request in the resolver's queue.
|
||||
@@ -159,8 +153,6 @@ func (r *Resolver) SetUpstreams(upstreams []net.Addr) {
|
||||
// If the queue is full, the request will be dropped and an error will be returned.
|
||||
func (r *Resolver) EnqueueRequest(request Packet) error {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return ErrClosed
|
||||
case r.queue <- request:
|
||||
return nil
|
||||
default:
|
||||
@@ -172,19 +164,18 @@ func (r *Resolver) EnqueueRequest(request Packet) error {
|
||||
// It blocks until a response is available and gives up ownership of the response payload.
|
||||
func (r *Resolver) NextResponse() (Packet, error) {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return Packet{}, ErrClosed
|
||||
case resp := <-r.responses:
|
||||
return resp, nil
|
||||
case err := <-r.errors:
|
||||
return Packet{}, err
|
||||
case <-r.closed:
|
||||
return Packet{}, ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve maps a given domain name to the IP address of the host that owns it,
|
||||
// if the IP address conforms to the DNS resource type given by tp (one of A, AAAA, ALL).
|
||||
// Resolve maps a given domain name to the IP address of the host that owns it.
|
||||
// The domain name must be in canonical form (with a trailing period).
|
||||
func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, error) {
|
||||
func (r *Resolver) Resolve(domain string) (netaddr.IP, dns.RCode, error) {
|
||||
r.mu.Lock()
|
||||
dnsMap := r.dnsMap
|
||||
r.mu.Unlock()
|
||||
@@ -193,38 +184,11 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e
|
||||
return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet
|
||||
}
|
||||
|
||||
anyHasSuffix := false
|
||||
for _, rootDomain := range dnsMap.rootDomains {
|
||||
if strings.HasSuffix(domain, rootDomain) {
|
||||
anyHasSuffix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !anyHasSuffix {
|
||||
return netaddr.IP{}, dns.RCodeRefused, nil
|
||||
}
|
||||
|
||||
addr, found := dnsMap.nameToIP[domain]
|
||||
if !found {
|
||||
return netaddr.IP{}, dns.RCodeNameError, nil
|
||||
}
|
||||
|
||||
// Refactoring note: this must happen after we check suffixes,
|
||||
// otherwise we will respond with NOTIMP to requests that should be forwarded.
|
||||
switch {
|
||||
case tp == dns.TypeA || tp == dns.TypeALL:
|
||||
if !addr.Is4() {
|
||||
return netaddr.IP{}, dns.RCodeSuccess, nil
|
||||
}
|
||||
return addr, dns.RCodeSuccess, nil
|
||||
case tp == dns.TypeAAAA || tp == dns.TypeALL:
|
||||
if !addr.Is6() {
|
||||
return netaddr.IP{}, dns.RCodeSuccess, nil
|
||||
}
|
||||
return addr, dns.RCodeSuccess, nil
|
||||
default:
|
||||
return netaddr.IP{}, dns.RCodeNotImplemented, errNotImplemented
|
||||
}
|
||||
return addr, dns.RCodeSuccess, nil
|
||||
}
|
||||
|
||||
// ResolveReverse returns the unique domain name that maps to the given address.
|
||||
@@ -245,56 +209,120 @@ func (r *Resolver) ResolveReverse(ip netaddr.IP) (string, dns.RCode, error) {
|
||||
}
|
||||
|
||||
func (r *Resolver) poll() {
|
||||
defer r.wg.Done()
|
||||
defer r.pollGroup.Done()
|
||||
|
||||
var packet Packet
|
||||
var (
|
||||
packet Packet
|
||||
err error
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return
|
||||
case packet = <-r.queue:
|
||||
// continue
|
||||
case <-r.closed:
|
||||
return
|
||||
}
|
||||
|
||||
out, err := r.respond(packet.Payload)
|
||||
|
||||
if err == errNotOurName {
|
||||
if r.forwarder != nil {
|
||||
err = r.forwarder.forward(packet)
|
||||
if err == nil {
|
||||
// forward will send response into r.responses, nothing to do.
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
err = errNotForwarding
|
||||
}
|
||||
}
|
||||
|
||||
packet.Payload, err = r.respond(packet.Payload)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return
|
||||
case r.errors <- err:
|
||||
// continue
|
||||
}
|
||||
} else {
|
||||
packet.Payload = out
|
||||
select {
|
||||
case <-r.closed:
|
||||
return
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case r.responses <- packet:
|
||||
// continue
|
||||
case <-r.closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// queryServer obtains a DNS response by querying the given server.
|
||||
func (r *Resolver) queryServer(ctx context.Context, server string, query []byte) ([]byte, error) {
|
||||
conn, err := r.dialer.DialContext(ctx, "udp", server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Interrupt the current operation when the context is cancelled.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
conn.SetDeadline(time.Unix(1, 0))
|
||||
}()
|
||||
|
||||
_, err = conn.Write(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]byte, maxResponseSize)
|
||||
n, err := conn.Read(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out[:n], nil
|
||||
}
|
||||
|
||||
// delegate forwards the query to all upstream nameservers and returns the first response.
|
||||
func (r *Resolver) delegate(query []byte) ([]byte, error) {
|
||||
r.mu.Lock()
|
||||
nameservers := r.nameservers
|
||||
r.mu.Unlock()
|
||||
|
||||
if len(nameservers) == 0 {
|
||||
return nil, errNoNameservers
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), delegateTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Common case, don't spawn goroutines.
|
||||
if len(nameservers) == 1 {
|
||||
return r.queryServer(ctx, nameservers[0], query)
|
||||
}
|
||||
|
||||
datach := make(chan []byte)
|
||||
for _, server := range nameservers {
|
||||
go func(s string) {
|
||||
resp, err := r.queryServer(ctx, s, query)
|
||||
// Only print errors not due to cancelation after first response.
|
||||
if err != nil && ctx.Err() != context.Canceled {
|
||||
r.logf("querying %s: %v", s, err)
|
||||
}
|
||||
|
||||
datach <- resp
|
||||
}(server)
|
||||
}
|
||||
|
||||
var response []byte
|
||||
for range nameservers {
|
||||
cur := <-datach
|
||||
if cur != nil && response == nil {
|
||||
// Received first successful response
|
||||
response = cur
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, errAllFailed
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Header dns.Header
|
||||
Question dns.Question
|
||||
// Name is the response to a PTR query.
|
||||
Name string
|
||||
// IP is the response to an A, AAAA, or ALL query.
|
||||
// IP is the response to an A, AAAA, or ANY query.
|
||||
IP netaddr.IP
|
||||
}
|
||||
|
||||
@@ -405,7 +433,7 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
if resp.IP.Is4() {
|
||||
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
|
||||
} else if resp.IP.Is6() {
|
||||
} else {
|
||||
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
|
||||
}
|
||||
case dns.TypePTR:
|
||||
@@ -418,34 +446,19 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
return builder.Finish()
|
||||
}
|
||||
|
||||
const (
|
||||
rdnsv4Suffix = ".in-addr.arpa."
|
||||
rdnsv6Suffix = ".ip6.arpa."
|
||||
var (
|
||||
rdnsv4Suffix = []byte(".in-addr.arpa.")
|
||||
rdnsv6Suffix = []byte(".ip6.arpa.")
|
||||
)
|
||||
|
||||
// rawNameToLower converts a raw DNS name to a string, lowercasing it.
|
||||
func rawNameToLower(name []byte) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(name))
|
||||
|
||||
for _, b := range name {
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
b = b - 'A' + 'a'
|
||||
}
|
||||
sb.WriteByte(b)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ptrNameToIPv4 transforms a PTR name representing an IPv4 address to said address.
|
||||
// Such names are IPv4 labels in reverse order followed by .in-addr.arpa.
|
||||
// For example,
|
||||
// 4.3.2.1.in-addr.arpa
|
||||
// is transformed to
|
||||
// 1.2.3.4
|
||||
func rdnsNameToIPv4(name string) (ip netaddr.IP, ok bool) {
|
||||
name = strings.TrimSuffix(name, rdnsv4Suffix)
|
||||
func rdnsNameToIPv4(name []byte) (ip netaddr.IP, ok bool) {
|
||||
name = bytes.TrimSuffix(name, rdnsv4Suffix)
|
||||
ip, err := netaddr.ParseIP(string(name))
|
||||
if err != nil {
|
||||
return netaddr.IP{}, false
|
||||
@@ -463,11 +476,11 @@ func rdnsNameToIPv4(name string) (ip netaddr.IP, ok bool) {
|
||||
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
|
||||
// is transformed to
|
||||
// 2001:db8::567:89ab
|
||||
func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
|
||||
func rdnsNameToIPv6(name []byte) (ip netaddr.IP, ok bool) {
|
||||
var b [32]byte
|
||||
var ipb [16]byte
|
||||
|
||||
name = strings.TrimSuffix(name, rdnsv6Suffix)
|
||||
name = bytes.TrimSuffix(name, rdnsv6Suffix)
|
||||
// 32 nibbles and 31 dots between them.
|
||||
if len(name) != 63 {
|
||||
return netaddr.IP{}, false
|
||||
@@ -501,39 +514,53 @@ func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
|
||||
|
||||
// respondReverse returns a DNS response to a PTR query.
|
||||
// It is assumed that resp.Question is populated by respond before this is called.
|
||||
func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]byte, error) {
|
||||
func (r *Resolver) respondReverse(query []byte, resp *response) ([]byte, error) {
|
||||
name := resp.Question.Name.Data[:resp.Question.Name.Length]
|
||||
|
||||
shouldDelegate := false
|
||||
|
||||
var ip netaddr.IP
|
||||
var ok bool
|
||||
var err error
|
||||
switch {
|
||||
case strings.HasSuffix(name, rdnsv4Suffix):
|
||||
case bytes.HasSuffix(name, rdnsv4Suffix):
|
||||
ip, ok = rdnsNameToIPv4(name)
|
||||
case strings.HasSuffix(name, rdnsv6Suffix):
|
||||
case bytes.HasSuffix(name, rdnsv6Suffix):
|
||||
ip, ok = rdnsNameToIPv6(name)
|
||||
default:
|
||||
return nil, errNotOurName
|
||||
shouldDelegate = true
|
||||
}
|
||||
|
||||
// It is more likely that we failed in parsing the name than that it is actually malformed.
|
||||
// To avoid frustrating users, just log and delegate.
|
||||
if !ok {
|
||||
r.logf("parsing rdns: malformed name: %s", name)
|
||||
return nil, errNotOurName
|
||||
// Without this conversion, escape analysis rules that resp escapes.
|
||||
r.logf("parsing rdns: malformed name: %s", resp.Question.Name.String())
|
||||
shouldDelegate = true
|
||||
}
|
||||
|
||||
resp.Name, resp.Header.RCode, err = r.ResolveReverse(ip)
|
||||
if err != nil {
|
||||
r.logf("resolving rdns: %v", ip, err)
|
||||
if !shouldDelegate {
|
||||
resp.Name, resp.Header.RCode, err = r.ResolveReverse(ip)
|
||||
if err != nil {
|
||||
r.logf("resolving rdns: %v", ip, err)
|
||||
}
|
||||
shouldDelegate = (resp.Header.RCode == dns.RCodeNameError)
|
||||
}
|
||||
if resp.Header.RCode == dns.RCodeNameError {
|
||||
return nil, errNotOurName
|
||||
|
||||
if shouldDelegate {
|
||||
out, err := r.delegate(query)
|
||||
if err != nil {
|
||||
r.logf("delegating rdns: %v", err)
|
||||
resp.Header.RCode = dns.RCodeServerFailure
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
// respond returns a DNS response to query if it can be resolved locally.
|
||||
// Otherwise, it returns errNotOurName.
|
||||
// respond returns a DNS response to query.
|
||||
func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
resp := new(response)
|
||||
|
||||
@@ -547,22 +574,35 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
resp.Header.RCode = dns.RCodeFormatError
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
rawName := resp.Question.Name.Data[:resp.Question.Name.Length]
|
||||
name := rawNameToLower(rawName)
|
||||
|
||||
// Always try to handle reverse lookups; delegate inside when not found.
|
||||
// This way, queries for exitent nodes do not leak,
|
||||
// but we behave gracefully if non-Tailscale nodes exist in CGNATRange.
|
||||
if resp.Question.Type == dns.TypePTR {
|
||||
return r.respondReverse(query, name, resp)
|
||||
return r.respondReverse(query, resp)
|
||||
}
|
||||
|
||||
resp.IP, resp.Header.RCode, err = r.Resolve(name, resp.Question.Type)
|
||||
// This return code is special: it requests forwarding.
|
||||
if resp.Header.RCode == dns.RCodeRefused {
|
||||
return nil, errNotOurName
|
||||
// Delegate forward lookups when not a subdomain of rootDomain.
|
||||
// We do this on bytes because Name.String() allocates.
|
||||
rawName := resp.Question.Name.Data[:resp.Question.Name.Length]
|
||||
if !bytes.HasSuffix(rawName, r.rootDomain) {
|
||||
out, err := r.delegate(query)
|
||||
if err != nil {
|
||||
r.logf("delegating: %v", err)
|
||||
resp.Header.RCode = dns.RCodeServerFailure
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
switch resp.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
name := resp.Question.Name.String()
|
||||
resp.IP, resp.Header.RCode, err = r.Resolve(name)
|
||||
default:
|
||||
resp.Header.RCode = dns.RCodeNotImplemented
|
||||
err = errNotImplemented
|
||||
}
|
||||
// We will not return this error: it is the sender's fault.
|
||||
if err != nil {
|
||||
r.logf("resolving: %v", err)
|
||||
|
||||
@@ -16,10 +16,9 @@ import (
|
||||
var dnsHandleFunc = dns.HandleFunc
|
||||
|
||||
// resolveToIP returns a handler function which 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 resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
// to queries of type A it receives with an A record containing ipv4
|
||||
// and to queries of type AAAA with an AAAA records containing ipv6.
|
||||
func resolveToIP(ipv4, ipv6 netaddr.IP) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
@@ -30,8 +29,7 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
question := req.Question[0]
|
||||
|
||||
var ans dns.RR
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
if question.Qtype == dns.TypeA {
|
||||
ans = &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
@@ -40,7 +38,7 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
},
|
||||
A: ipv4.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
} else {
|
||||
ans = &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
@@ -49,18 +47,9 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,11 @@ package tsdns
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
var testipv4 = netaddr.IPv4(1, 2, 3, 4)
|
||||
@@ -26,10 +24,9 @@ var testipv6 = netaddr.IPv6Raw([16]byte{
|
||||
|
||||
var dnsMap = NewMap(
|
||||
map[string]netaddr.IP{
|
||||
"test1.ipn.dev.": testipv4,
|
||||
"test2.ipn.dev.": testipv6,
|
||||
"test1.ipn.dev": testipv4,
|
||||
"test2.ipn.dev": testipv6,
|
||||
},
|
||||
[]string{"ipn.dev."},
|
||||
)
|
||||
|
||||
func dnspacket(domain string, tp dns.Type) []byte {
|
||||
@@ -48,64 +45,49 @@ func dnspacket(domain string, tp dns.Type) []byte {
|
||||
return payload
|
||||
}
|
||||
|
||||
type dnsResponse struct {
|
||||
ip netaddr.IP
|
||||
name string
|
||||
rcode dns.RCode
|
||||
}
|
||||
|
||||
func unpackResponse(payload []byte) (dnsResponse, error) {
|
||||
var response dnsResponse
|
||||
func extractipcode(response []byte) (netaddr.IP, dns.RCode, error) {
|
||||
var ip netaddr.IP
|
||||
var parser dns.Parser
|
||||
|
||||
h, err := parser.Start(payload)
|
||||
h, err := parser.Start(response)
|
||||
if err != nil {
|
||||
return response, err
|
||||
return ip, 0, err
|
||||
}
|
||||
|
||||
if !h.Response {
|
||||
return response, errors.New("not a response")
|
||||
return ip, 0, errors.New("not a response")
|
||||
}
|
||||
|
||||
response.rcode = h.RCode
|
||||
if response.rcode != dns.RCodeSuccess {
|
||||
return response, nil
|
||||
if h.RCode != dns.RCodeSuccess {
|
||||
return ip, h.RCode, nil
|
||||
}
|
||||
|
||||
err = parser.SkipAllQuestions()
|
||||
if err != nil {
|
||||
return response, err
|
||||
return ip, 0, err
|
||||
}
|
||||
|
||||
ah, err := parser.AnswerHeader()
|
||||
if err != nil {
|
||||
return response, err
|
||||
return ip, 0, err
|
||||
}
|
||||
|
||||
switch ah.Type {
|
||||
case dns.TypeA:
|
||||
res, err := parser.AResource()
|
||||
if err != nil {
|
||||
return response, err
|
||||
return ip, 0, err
|
||||
}
|
||||
response.ip = netaddr.IPv4(res.A[0], res.A[1], res.A[2], res.A[3])
|
||||
ip = netaddr.IPv4(res.A[0], res.A[1], res.A[2], res.A[3])
|
||||
case dns.TypeAAAA:
|
||||
res, err := parser.AAAAResource()
|
||||
if err != nil {
|
||||
return response, err
|
||||
return ip, 0, err
|
||||
}
|
||||
response.ip = netaddr.IPv6Raw(res.AAAA)
|
||||
case dns.TypeNS:
|
||||
res, err := parser.NSResource()
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response.name = res.NS.String()
|
||||
ip = netaddr.IPv6Raw(res.AAAA)
|
||||
default:
|
||||
return response, errors.New("type not in {A, AAAA, NS}")
|
||||
return ip, 0, errors.New("type not in {A, AAAA}")
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return ip, h.RCode, nil
|
||||
}
|
||||
|
||||
func syncRespond(r *Resolver, query []byte) ([]byte, error) {
|
||||
@@ -138,7 +120,8 @@ func TestRDNSNameToIPv4(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, ok := rdnsNameToIPv4(tt.input)
|
||||
name := []byte(tt.input)
|
||||
ip, ok := rdnsNameToIPv4(name)
|
||||
if ok != tt.wantOK {
|
||||
t.Errorf("ok = %v; want %v", ok, tt.wantOK)
|
||||
} else if ok && ip != tt.wantIP {
|
||||
@@ -183,7 +166,8 @@ func TestRDNSNameToIPv6(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, ok := rdnsNameToIPv6(tt.input)
|
||||
name := []byte(tt.input)
|
||||
ip, ok := rdnsNameToIPv6(name)
|
||||
if ok != tt.wantOK {
|
||||
t.Errorf("ok = %v; want %v", ok, tt.wantOK)
|
||||
} else if ok && ip != tt.wantIP {
|
||||
@@ -194,31 +178,25 @@ func TestRDNSNameToIPv6(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: false})
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetMap(dnsMap)
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Start()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
qname string
|
||||
qtype dns.Type
|
||||
ip netaddr.IP
|
||||
code dns.RCode
|
||||
name string
|
||||
domain string
|
||||
ip netaddr.IP
|
||||
code dns.RCode
|
||||
}{
|
||||
{"ipv4", "test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess},
|
||||
{"ipv6", "test2.ipn.dev.", dns.TypeAAAA, testipv6, dns.RCodeSuccess},
|
||||
{"no-ipv6", "test1.ipn.dev.", dns.TypeAAAA, netaddr.IP{}, dns.RCodeSuccess},
|
||||
{"nxdomain", "test3.ipn.dev.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"foreign domain", "google.com.", dns.TypeA, netaddr.IP{}, dns.RCodeRefused},
|
||||
{"ipv4", "test1.ipn.dev.", testipv4, dns.RCodeSuccess},
|
||||
{"ipv6", "test2.ipn.dev.", testipv6, dns.RCodeSuccess},
|
||||
{"nxdomain", "test3.ipn.dev.", netaddr.IP{}, dns.RCodeNameError},
|
||||
{"foreign domain", "google.com.", netaddr.IP{}, dns.RCodeNameError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, code, err := r.Resolve(tt.qname, tt.qtype)
|
||||
ip, code, err := r.Resolve(tt.domain)
|
||||
if err != nil {
|
||||
t.Errorf("err = %v; want nil", err)
|
||||
}
|
||||
@@ -234,13 +212,9 @@ func TestResolve(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveReverse(t *testing.T) {
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: false})
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetMap(dnsMap)
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Start()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -270,10 +244,7 @@ func TestResolveReverse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDelegate(t *testing.T) {
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
||||
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6))
|
||||
dnsHandleFunc("nxdomain.site.", resolveToNXDOMAIN)
|
||||
|
||||
v4server, v4errch := serveDNS("127.0.0.1:0")
|
||||
@@ -300,157 +271,49 @@ func TestDelegate(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: true})
|
||||
r.SetMap(dnsMap)
|
||||
r.SetUpstreams([]net.Addr{
|
||||
v4server.PacketConn.LocalAddr(),
|
||||
v6server.PacketConn.LocalAddr(),
|
||||
r := NewResolver(t.Logf, "ipn.dev")
|
||||
r.SetNameservers([]string{
|
||||
v4server.PacketConn.LocalAddr().String(),
|
||||
v6server.PacketConn.LocalAddr().String(),
|
||||
})
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Start()
|
||||
|
||||
tests := []struct {
|
||||
title string
|
||||
query []byte
|
||||
response dnsResponse
|
||||
name string
|
||||
query []byte
|
||||
ip netaddr.IP
|
||||
code dns.RCode
|
||||
}{
|
||||
{
|
||||
"ipv4",
|
||||
dnspacket("test.site.", dns.TypeA),
|
||||
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv6",
|
||||
dnspacket("test.site.", dns.TypeAAAA),
|
||||
dnsResponse{ip: testipv6, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ns",
|
||||
dnspacket("test.site.", dns.TypeNS),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"nxdomain",
|
||||
dnspacket("nxdomain.site.", dns.TypeA),
|
||||
dnsResponse{rcode: dns.RCodeNameError},
|
||||
},
|
||||
{"ipv4", dnspacket("test.site.", dns.TypeA), testipv4, dns.RCodeSuccess},
|
||||
{"ipv6", dnspacket("test.site.", dns.TypeAAAA), testipv6, dns.RCodeSuccess},
|
||||
{"nxdomain", dnspacket("nxdomain.site.", dns.TypeA), netaddr.IP{}, dns.RCodeNameError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.title, func(t *testing.T) {
|
||||
payload, err := syncRespond(r, tt.query)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp, err := syncRespond(r, tt.query)
|
||||
if err != nil {
|
||||
t.Errorf("err = %v; want nil", err)
|
||||
return
|
||||
}
|
||||
response, err := unpackResponse(payload)
|
||||
ip, code, err := extractipcode(resp)
|
||||
if err != nil {
|
||||
t.Errorf("extract: err = %v; want nil (in %x)", err, payload)
|
||||
t.Errorf("extract: err = %v; want nil (in %x)", err, resp)
|
||||
return
|
||||
}
|
||||
if response.rcode != tt.response.rcode {
|
||||
t.Errorf("rcode = %v; want %v", response.rcode, tt.response.rcode)
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if response.ip != tt.response.ip {
|
||||
t.Errorf("ip = %v; want %v", response.ip, tt.response.ip)
|
||||
}
|
||||
if response.name != tt.response.name {
|
||||
t.Errorf("name = %v; want %v", response.name, tt.response.name)
|
||||
if ip != tt.ip {
|
||||
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateCollision(t *testing.T) {
|
||||
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
||||
|
||||
server, errch := serveDNS("127.0.0.1:0")
|
||||
defer func() {
|
||||
if err := <-errch; err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if server == nil {
|
||||
return
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: true})
|
||||
r.SetMap(dnsMap)
|
||||
r.SetUpstreams([]net.Addr{server.PacketConn.LocalAddr()})
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
packets := []struct {
|
||||
qname string
|
||||
qtype dns.Type
|
||||
addr netaddr.IPPort
|
||||
}{
|
||||
{"test.site.", dns.TypeA, netaddr.IPPort{IP: netaddr.IPv4(1, 1, 1, 1), Port: 1001}},
|
||||
{"test.site.", dns.TypeAAAA, netaddr.IPPort{IP: netaddr.IPv4(1, 1, 1, 1), Port: 1002}},
|
||||
}
|
||||
|
||||
// packets will have the same dns txid.
|
||||
for _, p := range packets {
|
||||
payload := dnspacket(p.qname, p.qtype)
|
||||
req := Packet{Payload: payload, Addr: p.addr}
|
||||
err := r.EnqueueRequest(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Despite the txid collision, the answer(s) should still match the query.
|
||||
resp, err := r.NextResponse()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var p dns.Parser
|
||||
_, err = p.Start(resp.Payload)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = p.SkipAllQuestions()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
ans, err := p.AllAnswers()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var wantType dns.Type
|
||||
switch ans[0].Body.(type) {
|
||||
case *dns.AResource:
|
||||
wantType = dns.TypeA
|
||||
case *dns.AAAAResource:
|
||||
wantType = dns.TypeAAAA
|
||||
default:
|
||||
t.Errorf("unexpected answer type: %T", ans[0].Body)
|
||||
}
|
||||
|
||||
for _, p := range packets {
|
||||
if p.qtype == wantType && p.addr != resp.Addr {
|
||||
t.Errorf("addr = %v; want %v", resp.Addr, p.addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentSetMap(t *testing.T) {
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: false})
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r := NewResolver(t.Logf, "ipn.dev.")
|
||||
r.Start()
|
||||
|
||||
// This is purely to ensure that Resolve does not race with SetMap.
|
||||
var wg sync.WaitGroup
|
||||
@@ -461,41 +324,22 @@ func TestConcurrentSetMap(t *testing.T) {
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
r.Resolve("test1.ipn.dev", dns.TypeA)
|
||||
r.Resolve("test1.ipn.dev")
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestConcurrentSetUpstreams(t *testing.T) {
|
||||
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
||||
func TestConcurrentSetNameservers(t *testing.T) {
|
||||
r := NewResolver(t.Logf, "ipn.dev.")
|
||||
r.Start()
|
||||
packet := dnspacket("google.com.", dns.TypeA)
|
||||
|
||||
server, errch := serveDNS("127.0.0.1:0")
|
||||
defer func() {
|
||||
if err := <-errch; err != nil {
|
||||
t.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if server == nil {
|
||||
return
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: true})
|
||||
r.SetMap(dnsMap)
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
packet := dnspacket("test.site.", dns.TypeA)
|
||||
// This is purely to ensure that delegation does not race with SetUpstreams.
|
||||
// This is purely to ensure that delegation does not race with SetNameservers.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
r.SetUpstreams([]net.Addr{server.PacketConn.LocalAddr()})
|
||||
r.SetNameservers([]string{"9.9.9.9:53"})
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -504,24 +348,7 @@ func TestConcurrentSetUpstreams(t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var allResponse = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
0x00, 0x01, // one answer
|
||||
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
||||
// Question:
|
||||
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
||||
0x00, 0xff, 0x00, 0x01, // type ALL, class IN
|
||||
// Answer:
|
||||
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
||||
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
||||
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
||||
0x00, 0x04, // length: 4 bytes
|
||||
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
||||
}
|
||||
|
||||
var ipv4Response = []byte{
|
||||
var validIPv4Response = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
@@ -538,7 +365,7 @@ var ipv4Response = []byte{
|
||||
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
||||
}
|
||||
|
||||
var ipv6Response = []byte{
|
||||
var validIPv6Response = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
@@ -556,24 +383,7 @@ var ipv6Response = []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xb, 0xc, 0xd, 0xe, 0xf,
|
||||
}
|
||||
|
||||
var ipv4UppercaseResponse = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
0x00, 0x01, // one answer
|
||||
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
||||
// Question:
|
||||
0x05, 0x54, 0x45, 0x53, 0x54, 0x31, 0x03, 0x49, 0x50, 0x4e, 0x03, 0x44, 0x45, 0x56, 0x00, // name
|
||||
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
||||
// Answer:
|
||||
0x05, 0x54, 0x45, 0x53, 0x54, 0x31, 0x03, 0x49, 0x50, 0x4e, 0x03, 0x44, 0x45, 0x56, 0x00, // name
|
||||
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
||||
0x00, 0x00, 0x02, 0x58, // TTL: 600
|
||||
0x00, 0x04, // length: 4 bytes
|
||||
0x01, 0x02, 0x03, 0x04, // A: 1.2.3.4
|
||||
}
|
||||
|
||||
var ptrResponse = []byte{
|
||||
var validPTRResponse = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
@@ -604,25 +414,10 @@ var nxdomainResponse = []byte{
|
||||
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
||||
}
|
||||
|
||||
var emptyResponse = []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x00, // flags: response, authoritative, no error
|
||||
0x00, 0x01, // one question
|
||||
0x00, 0x00, // no answers
|
||||
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
||||
// Question:
|
||||
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name
|
||||
0x00, 0x1c, 0x00, 0x01, // type AAAA, class IN
|
||||
}
|
||||
|
||||
func TestFull(t *testing.T) {
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: false})
|
||||
r := NewResolver(t.Logf, "ipn.dev.")
|
||||
r.SetMap(dnsMap)
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Start()
|
||||
|
||||
// One full packet and one error packet
|
||||
tests := []struct {
|
||||
@@ -630,13 +425,10 @@ func TestFull(t *testing.T) {
|
||||
request []byte
|
||||
response []byte
|
||||
}{
|
||||
{"all", dnspacket("test1.ipn.dev.", dns.TypeALL), allResponse},
|
||||
{"ipv4", dnspacket("test1.ipn.dev.", dns.TypeA), ipv4Response},
|
||||
{"ipv6", dnspacket("test2.ipn.dev.", dns.TypeAAAA), ipv6Response},
|
||||
{"no-ipv6", dnspacket("test1.ipn.dev.", dns.TypeAAAA), emptyResponse},
|
||||
{"upper", dnspacket("TEST1.IPN.DEV.", dns.TypeA), ipv4UppercaseResponse},
|
||||
{"ptr", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), ptrResponse},
|
||||
{"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA), nxdomainResponse},
|
||||
{"ipv4", dnspacket("test1.ipn.dev.", dns.TypeA), validIPv4Response},
|
||||
{"ipv6", dnspacket("test2.ipn.dev.", dns.TypeAAAA), validIPv6Response},
|
||||
{"ptr", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), validPTRResponse},
|
||||
{"error", dnspacket("test3.ipn.dev.", dns.TypeA), nxdomainResponse},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -653,13 +445,9 @@ func TestFull(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAllocs(t *testing.T) {
|
||||
r := NewResolver(ResolverConfig{Logf: t.Logf, Forward: false})
|
||||
r := NewResolver(t.Logf, "ipn.dev.")
|
||||
r.SetMap(dnsMap)
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Start()
|
||||
|
||||
// It is seemingly pointless to test allocs in the delegate path,
|
||||
// as dialer.Dial -> Read -> Write alone comprise 12 allocs.
|
||||
@@ -668,8 +456,8 @@ func TestAllocs(t *testing.T) {
|
||||
query []byte
|
||||
want int
|
||||
}{
|
||||
// Name lowercasing and response slice created by dns.NewBuilder.
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA), 2},
|
||||
// The only alloc is the slice created by dns.NewBuilder.
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA), 1},
|
||||
// 3 extra allocs in rdnsNameToIPv4 and one in marshalPTRRecord (dns.NewName).
|
||||
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), 5},
|
||||
}
|
||||
@@ -685,28 +473,9 @@ func TestAllocs(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkFull(b *testing.B) {
|
||||
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
||||
|
||||
server, errch := serveDNS("127.0.0.1:0")
|
||||
defer func() {
|
||||
if err := <-errch; err != nil {
|
||||
b.Errorf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if server == nil {
|
||||
return
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
r := NewResolver(ResolverConfig{Logf: b.Logf, Forward: true})
|
||||
r := NewResolver(b.Logf, "ipn.dev.")
|
||||
r.SetMap(dnsMap)
|
||||
r.SetUpstreams([]net.Addr{server.PacketConn.LocalAddr()})
|
||||
|
||||
if err := r.Start(); err != nil {
|
||||
b.Fatalf("start: %v", err)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Start()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -714,7 +483,7 @@ func BenchmarkFull(b *testing.B) {
|
||||
}{
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA)},
|
||||
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR)},
|
||||
{"delegated", dnspacket("test.site.", dns.TypeA)},
|
||||
{"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -61,6 +61,9 @@ const (
|
||||
magicDNSPort = 53
|
||||
)
|
||||
|
||||
// magicDNSDomain is the parent domain for Tailscale nodes.
|
||||
const magicDNSDomain = "b.tailscale.net."
|
||||
|
||||
// Lazy wireguard-go configuration parameters.
|
||||
const (
|
||||
// lazyPeerIdleThreshold is the idle duration after
|
||||
@@ -105,18 +108,16 @@ type userspaceEngine struct {
|
||||
lastEngineSigFull string // of full wireguard config
|
||||
lastEngineSigTrim string // of trimmed wireguard config
|
||||
recvActivityAt map[tailcfg.DiscoKey]time.Time
|
||||
trimmedDisco map[tailcfg.DiscoKey]bool // set of disco keys of peers currently excluded from wireguard config
|
||||
sentActivityAt map[packet.IP]*int64 // value is atomic int64 of unixtime
|
||||
sentActivityAt map[packet.IP]*int64 // value is atomic int64 of unixtime
|
||||
destIPActivityFuncs map[packet.IP]func()
|
||||
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
closing bool // Close was called (even if we're still closing)
|
||||
statusCallback StatusCallback
|
||||
linkChangeCallback func(major bool, newState *interfaces.State)
|
||||
peerSequence []wgcfg.Key
|
||||
endpoints []string
|
||||
pingers map[wgcfg.Key]*pinger // legacy pingers for pre-discovery peers
|
||||
linkState *interfaces.State
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
closing bool // Close was called (even if we're still closing)
|
||||
statusCallback StatusCallback
|
||||
peerSequence []wgcfg.Key
|
||||
endpoints []string
|
||||
pingers map[wgcfg.Key]*pinger // legacy pingers for pre-discovery peers
|
||||
linkState *interfaces.State
|
||||
|
||||
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
|
||||
}
|
||||
@@ -200,22 +201,17 @@ func NewUserspaceEngineAdvanced(conf EngineConfig) (Engine, error) {
|
||||
func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
logf := conf.Logf
|
||||
|
||||
rconf := tsdns.ResolverConfig{
|
||||
Logf: conf.Logf,
|
||||
Forward: true,
|
||||
}
|
||||
e := &userspaceEngine{
|
||||
timeNow: time.Now,
|
||||
logf: logf,
|
||||
reqCh: make(chan struct{}, 1),
|
||||
waitCh: make(chan struct{}),
|
||||
tundev: tstun.WrapTUN(logf, conf.TUN),
|
||||
resolver: tsdns.NewResolver(rconf),
|
||||
resolver: tsdns.NewResolver(logf, magicDNSDomain),
|
||||
pingers: make(map[wgcfg.Key]*pinger),
|
||||
}
|
||||
e.localAddrs.Store(map[packet.IP]bool{})
|
||||
e.linkState, _ = getLinkState()
|
||||
logf("link state: %+v", e.linkState)
|
||||
|
||||
// Respond to all pings only in fake mode.
|
||||
if conf.Fake {
|
||||
@@ -241,7 +237,6 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
Logf: logf,
|
||||
Port: conf.ListenPort,
|
||||
EndpointsFunc: endpointsFn,
|
||||
DERPActiveFunc: e.RequestStatus,
|
||||
IdleFunc: e.tundev.IdleDuration,
|
||||
NoteRecvActivity: e.noteReceiveActivity,
|
||||
}
|
||||
@@ -573,10 +568,7 @@ func (e *userspaceEngine) pinger(peerKey wgcfg.Key, ips []wgcfg.IP) {
|
||||
p.run(ctx, peerKey, ips, srcIP)
|
||||
}
|
||||
|
||||
var (
|
||||
debugTrimWireguardEnv = os.Getenv("TS_DEBUG_TRIM_WIREGUARD")
|
||||
debugTrimWireguard, _ = strconv.ParseBool(debugTrimWireguardEnv)
|
||||
)
|
||||
var debugTrimWireguard, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_TRIM_WIREGUARD"))
|
||||
|
||||
// forceFullWireguardConfig reports whether we should give wireguard
|
||||
// our full network map, even for inactive peers
|
||||
@@ -586,13 +578,9 @@ var (
|
||||
// and we haven't got enough time testing it.
|
||||
func forceFullWireguardConfig(numPeers int) bool {
|
||||
// Did the user explicitly enable trimmming via the environment variable knob?
|
||||
if debugTrimWireguardEnv != "" {
|
||||
return !debugTrimWireguard
|
||||
if debugTrimWireguard {
|
||||
return false
|
||||
}
|
||||
if opt := controlclient.TrimWGConfig(); opt != "" {
|
||||
return !opt.EqualBool(true)
|
||||
}
|
||||
|
||||
// On iOS with large networks, it's critical, so turn on trimming.
|
||||
// Otherwise we run out of memory from wireguard-go goroutine stacks+buffers.
|
||||
// This will be the default later for all platforms and network sizes.
|
||||
@@ -600,7 +588,7 @@ func forceFullWireguardConfig(numPeers int) bool {
|
||||
if iOS && numPeers > 50 {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// isTrimmablePeer reports whether p is a peer that we can trim out of the
|
||||
@@ -638,11 +626,9 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
|
||||
e.wgLock.Lock()
|
||||
defer e.wgLock.Unlock()
|
||||
|
||||
if _, ok := e.recvActivityAt[dk]; !ok {
|
||||
was, ok := e.recvActivityAt[dk]
|
||||
if !ok {
|
||||
// Not a trimmable peer we care about tracking. (See isTrimmablePeer)
|
||||
if e.trimmedDisco[dk] {
|
||||
e.logf("wgengine: [unexpected] noteReceiveActivity called on idle discokey %v that's not in recvActivityAt", dk.ShortString())
|
||||
}
|
||||
return
|
||||
}
|
||||
now := e.timeNow()
|
||||
@@ -654,8 +640,7 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
|
||||
// lazyPeerIdleThreshold without the divide by 2, but
|
||||
// maybeReconfigWireguardLocked is cheap enough to call every
|
||||
// couple minutes (just not on every packet).
|
||||
if e.trimmedDisco[dk] {
|
||||
e.logf("wgengine: idle peer %v now active, reconfiguring wireguard", dk.ShortString())
|
||||
if was.IsZero() || now.Sub(was) > lazyPeerIdleThreshold/2 {
|
||||
e.maybeReconfigWireguardLocked()
|
||||
}
|
||||
}
|
||||
@@ -723,8 +708,6 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
|
||||
trackDisco := make([]tailcfg.DiscoKey, 0, len(full.Peers))
|
||||
trackIPs := make([]wgcfg.IP, 0, len(full.Peers))
|
||||
|
||||
trimmedDisco := map[tailcfg.DiscoKey]bool{} // TODO: don't re-alloc this map each time
|
||||
|
||||
for i := range full.Peers {
|
||||
p := &full.Peers[i]
|
||||
if !isTrimmablePeer(p, len(full.Peers)) {
|
||||
@@ -737,8 +720,6 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
|
||||
trackIPs = append(trackIPs, tsIP)
|
||||
if e.isActiveSince(dk, tsIP, activeCutoff) {
|
||||
min.Peers = append(min.Peers, *p)
|
||||
} else {
|
||||
trimmedDisco[dk] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,8 +728,6 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.trimmedDisco = trimmedDisco
|
||||
|
||||
e.updateActivityMapsLocked(trackDisco, trackIPs)
|
||||
|
||||
e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers))
|
||||
@@ -869,16 +848,11 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config)
|
||||
if routerChanged {
|
||||
if routerCfg.DNS.Proxied {
|
||||
ips := routerCfg.DNS.Nameservers
|
||||
upstreams := make([]net.Addr, len(ips))
|
||||
nameservers := make([]string, len(ips))
|
||||
for i, ip := range ips {
|
||||
stdIP := ip.IPAddr()
|
||||
upstreams[i] = &net.UDPAddr{
|
||||
IP: stdIP.IP,
|
||||
Port: 53,
|
||||
Zone: stdIP.Zone,
|
||||
}
|
||||
nameservers[i] = net.JoinHostPort(ip.String(), "53")
|
||||
}
|
||||
e.resolver.SetUpstreams(upstreams)
|
||||
e.resolver.SetNameservers(nameservers)
|
||||
routerCfg.DNS.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
|
||||
}
|
||||
e.logf("wgengine: Reconfig: configuring router")
|
||||
@@ -1111,15 +1085,15 @@ func (e *userspaceEngine) Wait() {
|
||||
<-e.waitCh
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) setLinkState(st *interfaces.State) (changed bool, cb func(major bool, newState *interfaces.State)) {
|
||||
func (e *userspaceEngine) setLinkState(st *interfaces.State) (changed bool) {
|
||||
if st == nil {
|
||||
return false, nil
|
||||
return false
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
changed = e.linkState == nil || !st.Equal(e.linkState)
|
||||
e.linkState = st
|
||||
return changed, e.linkChangeCallback
|
||||
return changed
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) LinkChange(isExpensive bool) {
|
||||
@@ -1129,13 +1103,9 @@ func (e *userspaceEngine) LinkChange(isExpensive bool) {
|
||||
return
|
||||
}
|
||||
cur.IsExpensive = isExpensive
|
||||
needRebind, linkChangeCallback := e.setLinkState(cur)
|
||||
needRebind := e.setLinkState(cur)
|
||||
|
||||
if needRebind {
|
||||
e.logf("LinkChange: major, rebinding. New state: %+v", cur)
|
||||
} else {
|
||||
e.logf("LinkChange: minor")
|
||||
}
|
||||
e.logf("LinkChange(isExpensive=%v); needsRebind=%v", isExpensive, needRebind)
|
||||
|
||||
why := "link-change-minor"
|
||||
if needRebind {
|
||||
@@ -1143,15 +1113,6 @@ func (e *userspaceEngine) LinkChange(isExpensive bool) {
|
||||
e.magicConn.Rebind()
|
||||
}
|
||||
e.magicConn.ReSTUN(why)
|
||||
if linkChangeCallback != nil {
|
||||
go linkChangeCallback(needRebind, cur)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) SetLinkChangeCallback(cb func(major bool, newState *interfaces.State)) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.linkChangeCallback = cb
|
||||
}
|
||||
|
||||
func getLinkState() (*interfaces.State, error) {
|
||||
@@ -1196,10 +1157,6 @@ func (e *userspaceEngine) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
e.magicConn.UpdateStatus(sb)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) {
|
||||
e.magicConn.Ping(ip, cb)
|
||||
}
|
||||
|
||||
// diagnoseTUNFailure is called if tun.CreateTUN fails, to poke around
|
||||
// the system and log some diagnostic info that might help debug why
|
||||
// TUN failed. Because TUN's already failed and things the program's
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
func TestNoteReceiveActivity(t *testing.T) {
|
||||
now := time.Unix(1, 0)
|
||||
tick := func(d time.Duration) { now = now.Add(d) }
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
confc := make(chan bool, 1)
|
||||
@@ -36,7 +37,6 @@ func TestNoteReceiveActivity(t *testing.T) {
|
||||
},
|
||||
tundev: new(tstun.TUN),
|
||||
testMaybeReconfigHook: func() { confc <- true },
|
||||
trimmedDisco: map[tailcfg.DiscoKey]bool{},
|
||||
}
|
||||
ra := e.recvActivityAt
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestNoteReceiveActivity(t *testing.T) {
|
||||
t.Fatalf("unexpected log write (and thus activity): %s", logBuf.Bytes())
|
||||
}
|
||||
|
||||
// Now track it, but don't mark it trimmed, so shouldn't update.
|
||||
// Now track it and expect updates.
|
||||
ra[dk] = time.Time{}
|
||||
e.noteReceiveActivity(dk)
|
||||
if len(ra) != 1 {
|
||||
@@ -60,20 +60,29 @@ func TestNoteReceiveActivity(t *testing.T) {
|
||||
if got := ra[dk]; got != now {
|
||||
t.Fatalf("time in map = %v; want %v", got, now)
|
||||
}
|
||||
if gotConf() {
|
||||
t.Fatalf("unexpected reconfig")
|
||||
}
|
||||
|
||||
// Now mark it trimmed and expect an update.
|
||||
e.trimmedDisco[dk] = true
|
||||
e.noteReceiveActivity(dk)
|
||||
if len(ra) != 1 {
|
||||
t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra))
|
||||
}
|
||||
if got := ra[dk]; got != now {
|
||||
t.Fatalf("time in map = %v; want %v", got, now)
|
||||
}
|
||||
if !gotConf() {
|
||||
t.Fatalf("didn't get expected reconfig")
|
||||
}
|
||||
|
||||
// With updates 1 second apart, don't expect a reconfig.
|
||||
for i := 0; i < 300; i++ {
|
||||
tick(time.Second)
|
||||
e.noteReceiveActivity(dk)
|
||||
if len(ra) != 1 {
|
||||
t.Fatalf("map len = %d; want 1", len(ra))
|
||||
}
|
||||
if got := ra[dk]; got != now {
|
||||
t.Fatalf("time in map = %v; want %v", got, now)
|
||||
}
|
||||
if gotConf() {
|
||||
t.Fatalf("unexpected reconfig")
|
||||
}
|
||||
}
|
||||
|
||||
// But if there's a big jump it should get an update.
|
||||
tick(3 * time.Minute)
|
||||
e.noteReceiveActivity(dk)
|
||||
if !gotConf() {
|
||||
t.Fatalf("expected config")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -101,9 +99,6 @@ func (e *watchdogEngine) RequestStatus() {
|
||||
func (e *watchdogEngine) LinkChange(isExpensive bool) {
|
||||
e.watchdog("LinkChange", func() { e.wrap.LinkChange(isExpensive) })
|
||||
}
|
||||
func (e *watchdogEngine) SetLinkChangeCallback(cb func(major bool, newState *interfaces.State)) {
|
||||
e.watchdog("SetLinkChangeCallback", func() { e.wrap.SetLinkChangeCallback(cb) })
|
||||
}
|
||||
func (e *watchdogEngine) SetDERPMap(m *tailcfg.DERPMap) {
|
||||
e.watchdog("SetDERPMap", func() { e.wrap.SetDERPMap(m) })
|
||||
}
|
||||
@@ -114,9 +109,6 @@ func (e *watchdogEngine) DiscoPublicKey() (k tailcfg.DiscoKey) {
|
||||
e.watchdog("DiscoPublicKey", func() { k = e.wrap.DiscoPublicKey() })
|
||||
return k
|
||||
}
|
||||
func (e *watchdogEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) {
|
||||
e.watchdog("Ping", func() { e.wrap.Ping(ip, cb) })
|
||||
}
|
||||
func (e *watchdogEngine) Close() {
|
||||
e.watchdog("Close", e.wrap.Close)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -118,10 +116,6 @@ type Engine interface {
|
||||
// new NetInfo summary is available.
|
||||
SetNetInfoCallback(NetInfoCallback)
|
||||
|
||||
// SetLinkChangeCallback sets the function to call when the
|
||||
// link state changes.
|
||||
SetLinkChangeCallback(func(major bool, newState *interfaces.State))
|
||||
|
||||
// DiscoPublicKey gets the public key used for path discovery
|
||||
// messages.
|
||||
DiscoPublicKey() tailcfg.DiscoKey
|
||||
@@ -129,8 +123,4 @@ type Engine interface {
|
||||
// UpdateStatus populates the network state using the provided
|
||||
// status builder.
|
||||
UpdateStatus(*ipnstate.StatusBuilder)
|
||||
|
||||
// Ping is a request to start a discovery ping with the peer handling
|
||||
// the given IP and then call cb with its ping latency & method.
|
||||
Ping(ip netaddr.IP, cb func(*ipnstate.PingResult))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user