Compare commits
2 Commits
vm
...
buildjet-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1691898a17 | ||
|
|
3d24611e32 |
10
.github/workflows/cifuzz.yml
vendored
10
.github/workflows/cifuzz.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: CIFuzz
|
||||
on: [pull_request]
|
||||
on:
|
||||
push:
|
||||
branches: [ main, release-branch/* ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'tailscale'
|
||||
fuzz-seconds: 300
|
||||
fuzz-seconds: 900
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -27,7 +27,7 @@ concurrency:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/cross-android.yml
vendored
2
.github/workflows/cross-android.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-wasm.yml
vendored
2
.github/workflows/cross-wasm.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/linux-race.yml
vendored
2
.github/workflows/linux-race.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
6
.github/workflows/linux.yml
vendored
6
.github/workflows/linux.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
@@ -38,10 +38,6 @@ jobs:
|
||||
|
||||
- name: Get QEMU
|
||||
run: |
|
||||
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
|
||||
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
|
||||
# use this PPA which brings in a modern qemu.
|
||||
sudo add-apt-repository -y ppa:jacob/virtualisation
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user
|
||||
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
4
.github/workflows/static-analysis.yml
vendored
4
.github/workflows/static-analysis.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
vet:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
staticcheck:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
|
||||
4
.github/workflows/vm.yml
vendored
4
.github/workflows/vm.yml
vendored
@@ -11,7 +11,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
ubuntu2004-LTS-cloud-base:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: [ self-hosted, linux, vm ]
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
env:
|
||||
HOME: "/tmp"
|
||||
TMPDIR: "/tmp"
|
||||
XDG_CACHE_HOME: "$HOME/.cache"
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-8vcpu
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
@@ -24,17 +24,11 @@ func New(socket string) (*BIRDClient, error) {
|
||||
return newWithTimeout(socket, responseTimeout)
|
||||
}
|
||||
|
||||
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
|
||||
func newWithTimeout(socket string, timeout time.Duration) (*BIRDClient, error) {
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
b := &BIRDClient{
|
||||
socket: socket,
|
||||
conn: conn,
|
||||
|
||||
@@ -267,47 +267,15 @@ func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) (
|
||||
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
|
||||
}
|
||||
|
||||
// BugReportOpts contains options to pass to the Tailscale daemon when
|
||||
// generating a bug report.
|
||||
type BugReportOpts struct {
|
||||
// Note contains an optional user-provided note to add to the logs.
|
||||
Note string
|
||||
|
||||
// Diagnose specifies whether to print additional diagnostic information to
|
||||
// the logs when generating this bugreport.
|
||||
Diagnose bool
|
||||
}
|
||||
|
||||
// BugReportWithOpts logs and returns a log marker that can be shared by the
|
||||
// user with support.
|
||||
//
|
||||
// The opts type specifies options to pass to the Tailscale daemon when
|
||||
// generating this bug report.
|
||||
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
|
||||
var qparams url.Values
|
||||
if opts.Note != "" {
|
||||
qparams.Set("note", opts.Note)
|
||||
}
|
||||
if opts.Diagnose {
|
||||
qparams.Set("diagnose", "true")
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
|
||||
body, err := lc.send(ctx, "POST", uri, 200, nil)
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
//
|
||||
// This is the same as calling BugReportWithOpts and only specifying the Note
|
||||
// field.
|
||||
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
|
||||
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
|
||||
}
|
||||
|
||||
// DebugAction invokes a debug action, such as "rebind" or "restun".
|
||||
// These are development tools and subject to change or removal over time.
|
||||
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
||||
|
||||
@@ -7,10 +7,8 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var bugReportCmd = &ffcli.Command{
|
||||
@@ -18,15 +16,6 @@ var bugReportCmd = &ffcli.Command{
|
||||
Exec: runBugReport,
|
||||
ShortHelp: "Print a shareable identifier to help diagnose issues",
|
||||
ShortUsage: "bugreport [note]",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("bugreport")
|
||||
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var bugReportArgs struct {
|
||||
diagnose bool
|
||||
}
|
||||
|
||||
func runBugReport(ctx context.Context, args []string) error {
|
||||
@@ -38,10 +27,7 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
default:
|
||||
return errors.New("unknown argumets")
|
||||
}
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{
|
||||
Note: note,
|
||||
Diagnose: bugReportArgs.diagnose,
|
||||
})
|
||||
logMarker, err := localClient.BugReport(ctx, note)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func runConfigureHost(ctx context.Context, args []string) error {
|
||||
if uid := os.Getuid(); uid != 0 {
|
||||
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
|
||||
}
|
||||
hi := hostinfo.New()
|
||||
hi:= hostinfo.New()
|
||||
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
|
||||
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
|
||||
if !isDSM6 && !isDSM7 {
|
||||
|
||||
@@ -109,7 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -190,8 +190,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
@@ -232,7 +230,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
@@ -275,7 +272,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
@@ -21,7 +22,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump())
|
||||
zw.Write(scrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
@@ -39,3 +40,83 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
|
||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func scrubbedGoroutineDump() []byte {
|
||||
var buf []byte
|
||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||
// to minimize the risk that we blow past our iOS memory limit.
|
||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
// It fit.
|
||||
break
|
||||
}
|
||||
}
|
||||
return scrubHex(buf)
|
||||
}
|
||||
|
||||
func scrubHex(buf []byte) []byte {
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
|
||||
foreachHexAddress(buf, func(in []byte) {
|
||||
if string(in) == "0x0" {
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
var ohx = []byte("0x")
|
||||
|
||||
// foreachHexAddress calls f with each subslice of b that matches
|
||||
// regexp `0x[0-9a-f]*`.
|
||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||
for len(b) > 0 {
|
||||
i := bytes.Index(b, ohx)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
b = b[i:]
|
||||
hx := hexPrefix(b)
|
||||
f(hx)
|
||||
b = b[len(hx):]
|
||||
}
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
if !isHexByte(c) {
|
||||
return b[:i]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isHexByte(b byte) bool {
|
||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package goroutines
|
||||
package controlclient
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestScrubbedGoroutineDump(t *testing.T) {
|
||||
t.Logf("Got:\n%s\n", ScrubbedGoroutineDump())
|
||||
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
|
||||
}
|
||||
|
||||
func TestScrubHex(t *testing.T) {
|
||||
@@ -1438,7 +1438,7 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := nc.post(ctx, "/machine/set-dns", &newReq)
|
||||
res, err := nc.post(ctx, "/machine/set-dns", req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,11 +29,9 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
|
||||
wsScheme := "wss"
|
||||
host := d.Hostname
|
||||
// If using a custom control server (on a non-standard port), prefer that.
|
||||
// This mirrors the port selection in newNoiseClient from noise.go.
|
||||
if d.HTTPPort != "" && d.HTTPPort != "80" && d.HTTPSPort == "443" {
|
||||
if host == "localhost" {
|
||||
wsScheme = "ws"
|
||||
host = net.JoinHostPort(host, d.HTTPPort)
|
||||
host = net.JoinHostPort(host, strDef(d.HTTPPort, "80"))
|
||||
}
|
||||
wsURL := &url.URL{
|
||||
Scheme: wsScheme,
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package doctor contains more in-depth healthchecks that can be run to aid in
|
||||
// diagnosing Tailscale issues.
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check is the interface defining a singular check.
|
||||
//
|
||||
// A check should log information that it gathers using the provided log
|
||||
// function, and should attempt to make as much progress as possible in error
|
||||
// conditions.
|
||||
type Check interface {
|
||||
// Name should return a name describing this check, in lower-kebab-case
|
||||
// (i.e. "my-check", not "MyCheck" or "my_check").
|
||||
Name() string
|
||||
// Run executes the check, logging diagnostic information to the
|
||||
// provided logger function.
|
||||
Run(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
// RunChecks runs a list of checks in parallel, and logs any returned errors
|
||||
// after all checks have returned.
|
||||
func RunChecks(ctx context.Context, log logger.Logf, checks ...Check) {
|
||||
if len(checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type namedErr struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
errs := make(chan namedErr, len(checks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(checks))
|
||||
for _, check := range checks {
|
||||
go func(c Check) {
|
||||
defer wg.Done()
|
||||
|
||||
plog := logger.WithPrefix(log, c.Name()+": ")
|
||||
errs <- namedErr{
|
||||
name: c.Name(),
|
||||
err: c.Run(ctx, plog),
|
||||
}
|
||||
}(check)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for n := range errs {
|
||||
if n.err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
log("check %s: %v", n.name, n.err)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckFunc creates a Check from a name and a function.
|
||||
func CheckFunc(name string, run func(context.Context, logger.Logf) error) Check {
|
||||
return checkFunc{name, run}
|
||||
}
|
||||
|
||||
type checkFunc struct {
|
||||
name string
|
||||
run func(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
func (c checkFunc) Name() string { return c.name }
|
||||
func (c checkFunc) Run(ctx context.Context, log logger.Logf) error { return c.run(ctx, log) }
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestRunChecks(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
)
|
||||
logf := func(format string, args ...any) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lines = append(lines, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
RunChecks(ctx, logf,
|
||||
testCheck1{},
|
||||
CheckFunc("testcheck2", func(_ context.Context, log logger.Logf) error {
|
||||
log("check 2")
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
c.Assert(lines, qt.Contains, "testcheck1: check 1")
|
||||
c.Assert(lines, qt.Contains, "testcheck2: check 2")
|
||||
}
|
||||
|
||||
type testCheck1 struct{}
|
||||
|
||||
func (t testCheck1) Name() string { return "testcheck1" }
|
||||
func (t testCheck1) Run(_ context.Context, log logger.Logf) error {
|
||||
log("check 1")
|
||||
return nil
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package routetable provides a doctor.Check that dumps the current system's
|
||||
// route table to the log.
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/net/routetable"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// MaxRoutes is the maximum number of routes that will be displayed.
|
||||
const MaxRoutes = 1000
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "routetable"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
rs, err := routetable.Get(MaxRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rs {
|
||||
logf("%s", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -10,28 +10,14 @@ import (
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON := func(v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/echo":
|
||||
// Test handler.
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
w.Write(body)
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||
case "/debug/prefs":
|
||||
writeJSON(b.Prefs())
|
||||
case "/debug/metrics":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
case "/ssh/usernames":
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
@@ -45,7 +31,8 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
writeJSON(res)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
default:
|
||||
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ import (
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/doctor"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -3686,19 +3684,3 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
io.WriteString(w, "</ul>\n")
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
|
||||
var checks []doctor.Check
|
||||
|
||||
checks = append(checks, routetable.Check{})
|
||||
|
||||
// TODO(andrew): more
|
||||
|
||||
numChecks := len(checks)
|
||||
checks = append(checks, doctor.CheckFunc("numchecks", func(_ context.Context, log logger.Logf) error {
|
||||
log("%d checks", numChecks)
|
||||
return nil
|
||||
}))
|
||||
|
||||
doctor.RunChecks(ctx, logf, checks...)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
// If the feature flag is not enabled, pretend we don't exist.
|
||||
return nil
|
||||
}
|
||||
if nm.SelfNode == nil {
|
||||
return errors.New("SelfNode missing")
|
||||
}
|
||||
|
||||
isEnabled := b.tka != nil
|
||||
wantEnabled := nm.TKAEnabled
|
||||
@@ -66,9 +69,8 @@ func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
|
||||
// Regardless of whether we are moving to disabled or enabled, we
|
||||
// need information from the tka bootstrap endpoint.
|
||||
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
|
||||
b.mu.Unlock()
|
||||
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
|
||||
bs, err := b.tkaFetchBootstrap(nm.SelfNode.ID, ourHead)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching bootstrap: %v", err)
|
||||
@@ -200,15 +202,9 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
if !b.CanSupportNetworkLock() {
|
||||
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
|
||||
}
|
||||
|
||||
var ourNodeKey key.NodePublic
|
||||
b.mu.Lock()
|
||||
if b.prefs != nil {
|
||||
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if ourNodeKey.IsZero() {
|
||||
return errors.New("no node-key: is tailscale logged in?")
|
||||
nm := b.NetMap()
|
||||
if nm == nil {
|
||||
return errors.New("no netmap: are you logged into tailscale?")
|
||||
}
|
||||
|
||||
// Generates a genesis AUM representing trust in the provided keys.
|
||||
@@ -230,7 +226,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
}
|
||||
|
||||
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
|
||||
initResp, err := b.tkaInitBegin(ourNodeKey, genesisAUM)
|
||||
initResp, err := b.tkaInitBegin(nm, genesisAUM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tka init-begin RPC: %w", err)
|
||||
}
|
||||
@@ -251,7 +247,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
}
|
||||
|
||||
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||
_, err = b.tkaInitFinish(ourNodeKey, sigs)
|
||||
_, err = b.tkaInitFinish(nm, sigs)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -274,11 +270,10 @@ func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeK
|
||||
return &sig, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
NodeID: nm.SelfNode.ID,
|
||||
GenesisAUM: aum.Serialize(),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
@@ -316,11 +311,10 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
NodeID: nm.SelfNode.ID,
|
||||
Signatures: nks,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
@@ -360,10 +354,9 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.
|
||||
|
||||
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
|
||||
// over noise. This is used to get values necessary to enable or disable TKA.
|
||||
func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
|
||||
func (b *LocalBackend) tkaFetchBootstrap(nodeID tailcfg.NodeID, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
|
||||
bootstrapReq := tailcfg.TKABootstrapRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
NodeID: nodeID,
|
||||
}
|
||||
if !head.IsZero() {
|
||||
head, err := head.MarshalText()
|
||||
|
||||
@@ -17,12 +17,10 @@ import (
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
)
|
||||
|
||||
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
||||
@@ -64,7 +62,6 @@ func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server,
|
||||
|
||||
func TestTKAEnablementFlow(t *testing.T) {
|
||||
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, getting a usable genesis AUM which
|
||||
// our mock server can communicate.
|
||||
@@ -86,11 +83,8 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("bootstrap nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
if body.NodeID != 420 {
|
||||
t.Errorf("bootstrap nodeID=%v, want 420", body.NodeID)
|
||||
}
|
||||
if body.Head != "" {
|
||||
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
|
||||
@@ -118,13 +112,11 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
SelfNode: &tailcfg.Node{ID: 420},
|
||||
TKAEnabled: true,
|
||||
TKAHead: tka.AUMHash{},
|
||||
})
|
||||
@@ -144,7 +136,6 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||||
temp := t.TempDir()
|
||||
os.Mkdir(filepath.Join(temp, "tka"), 0755)
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, to seed local state.
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
@@ -162,7 +153,6 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
returnWrongSecret := false
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
@@ -171,11 +161,14 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
var disablement []byte
|
||||
switch body.NodeID {
|
||||
case 42:
|
||||
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
|
||||
case 420:
|
||||
disablement = disablementSecret
|
||||
default:
|
||||
t.Errorf("bootstrap nodeID=%v, wanted 42 or 420", body.NodeID)
|
||||
}
|
||||
var head tka.AUMHash
|
||||
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
|
||||
@@ -185,13 +178,6 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
t.Errorf("reported head = %x, want %x", head, authority.Head())
|
||||
}
|
||||
|
||||
var disablement []byte
|
||||
if returnWrongSecret {
|
||||
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
|
||||
} else {
|
||||
disablement = disablementSecret
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
out := tailcfg.TKABootstrapResponse{
|
||||
DisablementSecret: disablement,
|
||||
@@ -217,15 +203,13 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
// Test that the wrong disablement secret does not shut down the authority.
|
||||
returnWrongSecret = true
|
||||
// NodeID == 42 indicates this scenario to our mock server.
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
SelfNode: &tailcfg.Node{ID: 42},
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
@@ -238,9 +222,10 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test the correct disablement secret shuts down the authority.
|
||||
returnWrongSecret = false
|
||||
// NodeID == 420 indicates this scenario to our mock server.
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
SelfNode: &tailcfg.Node{ID: 420},
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
|
||||
@@ -221,9 +221,6 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
if note := r.FormValue("note"); len(note) > 0 {
|
||||
h.logf("user bugreport note: %s", note)
|
||||
}
|
||||
if defBool(r.FormValue("diagnose"), false) {
|
||||
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, logMarker)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -100,44 +97,6 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface to ensure that Hosts is
|
||||
// printed correctly (i.e. not as a bunch of pointers).
|
||||
//
|
||||
// Fixes https://github.com/tailscale/tailscale/issues/5669
|
||||
func (a OSConfig) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
w.WriteString(`{Nameservers:[`)
|
||||
for i, ns := range a.Nameservers {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", ns)
|
||||
}
|
||||
w.WriteString(`] SearchDomains:[`)
|
||||
for i, domain := range a.SearchDomains {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
w.WriteString(`] MatchDomains:[`)
|
||||
for i, domain := range a.MatchDomains {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
w.WriteString(`] Hosts:[`)
|
||||
for i, host := range a.Hosts {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", host)
|
||||
}
|
||||
w.WriteString(`]}`)
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// ErrGetBaseConfigNotSupported is the error
|
||||
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
|
||||
// doesn't support reading the underlying configuration out of the OS.
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func TestOSConfigPrintable(t *testing.T) {
|
||||
ocfg := OSConfig{
|
||||
Hosts: []*HostEntry{
|
||||
{
|
||||
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}),
|
||||
Hosts: []string{"server", "client"},
|
||||
},
|
||||
{
|
||||
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 4}),
|
||||
Hosts: []string{"otherhost"},
|
||||
},
|
||||
},
|
||||
Nameservers: []netip.Addr{
|
||||
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
|
||||
},
|
||||
SearchDomains: []dnsname.FQDN{
|
||||
dnsname.FQDN("foo.beta.tailscale.net."),
|
||||
dnsname.FQDN("bar.beta.tailscale.net."),
|
||||
},
|
||||
MatchDomains: []dnsname.FQDN{
|
||||
dnsname.FQDN("ts.com."),
|
||||
},
|
||||
}
|
||||
s := fmt.Sprintf("%+v", ocfg)
|
||||
|
||||
const expected = `{Nameservers:[8.8.8.8] SearchDomains:[foo.beta.tailscale.net. bar.beta.tailscale.net.] MatchDomains:[ts.com.] Hosts:[&{Addr:100.1.2.3 Hosts:[server client]} &{Addr:100.1.2.4 Hosts:[otherhost]}]}`
|
||||
if s != expected {
|
||||
t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected)
|
||||
}
|
||||
}
|
||||
@@ -1188,8 +1188,6 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
var ip netip.Addr
|
||||
|
||||
dc := derphttp.NewNetcheckClient(c.logf)
|
||||
defer dc.Close()
|
||||
|
||||
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
|
||||
@@ -61,7 +61,7 @@ func (l *Listener) Accept() (net.Conn, error) {
|
||||
// The provided Context must be non-nil. If the context expires before the
|
||||
// connection is complete, an error is returned. Once successfully connected
|
||||
// any expiration of the context will not affect the connection.
|
||||
func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn, err error) {
|
||||
func (l *Listener) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if !strings.HasSuffix(network, "tcp") {
|
||||
return nil, net.UnknownNetworkError(network)
|
||||
}
|
||||
@@ -72,13 +72,6 @@ func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn,
|
||||
}
|
||||
}
|
||||
c, s := NewConn(addr, bufferSize)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
s.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package routetable provides functions that operate on the system's route
|
||||
// table.
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultRouteIPv4 = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
|
||||
defaultRouteIPv6 = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
|
||||
)
|
||||
|
||||
// RouteEntry contains common cross-platform fields describing an entry in the
|
||||
// system route table.
|
||||
type RouteEntry struct {
|
||||
// Family is the IP family of the route; it will be either 4 or 6.
|
||||
Family int
|
||||
// Type is the type of this route.
|
||||
Type RouteType
|
||||
// Dst is the destination of the route.
|
||||
Dst RouteDestination
|
||||
// Gatewayis the gateway address specified for this route.
|
||||
// This value will be invalid (where !r.Gateway.IsValid()) in cases
|
||||
// where there is no gateway address for this route.
|
||||
Gateway netip.Addr
|
||||
// Interface is the name of the network interface to use when sending
|
||||
// packets that match this route. This field can be empty.
|
||||
Interface string
|
||||
// Sys contains platform-specific information about this route.
|
||||
Sys any
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface.
|
||||
func (r RouteEntry) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
switch r.Family {
|
||||
case 4:
|
||||
fmt.Fprintf(w, "{Family: IPv4")
|
||||
case 6:
|
||||
fmt.Fprintf(w, "{Family: IPv6")
|
||||
default:
|
||||
fmt.Fprintf(w, "{Family: unknown(%d)", r.Family)
|
||||
}
|
||||
|
||||
// Match 'ip route' and other tools by not printing the route
|
||||
// type if it's a unicast route.
|
||||
if r.Type != RouteTypeUnicast {
|
||||
fmt.Fprintf(w, ", Type: %s", r.Type)
|
||||
}
|
||||
|
||||
if r.Dst.IsValid() {
|
||||
fmt.Fprintf(w, ", Dst: %s", r.Dst)
|
||||
} else {
|
||||
w.WriteString(", Dst: invalid")
|
||||
}
|
||||
|
||||
if r.Gateway.IsValid() {
|
||||
fmt.Fprintf(w, ", Gateway: %s", r.Gateway)
|
||||
}
|
||||
|
||||
if r.Interface != "" {
|
||||
fmt.Fprintf(w, ", Interface: %s", r.Interface)
|
||||
}
|
||||
|
||||
if r.Sys != nil {
|
||||
var formatVerb string
|
||||
switch {
|
||||
case f.Flag('#'):
|
||||
formatVerb = "%#v"
|
||||
case f.Flag('+'):
|
||||
formatVerb = "%+v"
|
||||
default:
|
||||
formatVerb = "%v"
|
||||
}
|
||||
fmt.Fprintf(w, ", Sys: "+formatVerb, r.Sys)
|
||||
}
|
||||
|
||||
w.WriteString("}")
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// RouteDestination is the destination of a route.
|
||||
//
|
||||
// This is similar to net/netip.Prefix, but also contains an optional IPv6
|
||||
// zone.
|
||||
type RouteDestination struct {
|
||||
netip.Prefix
|
||||
Zone string
|
||||
}
|
||||
|
||||
func (r RouteDestination) String() string {
|
||||
ip := r.Prefix.Addr()
|
||||
if r.Zone != "" {
|
||||
ip = ip.WithZone(r.Zone)
|
||||
}
|
||||
return ip.String() + "/" + strconv.Itoa(r.Prefix.Bits())
|
||||
}
|
||||
|
||||
// RouteType describes the type of a route.
|
||||
type RouteType int
|
||||
|
||||
const (
|
||||
// RouteTypeUnspecified is the unspecified route type.
|
||||
RouteTypeUnspecified RouteType = iota
|
||||
// RouteTypeLocal indicates that the destination of this route is an
|
||||
// address that belongs to this system.
|
||||
RouteTypeLocal
|
||||
// RouteTypeUnicast indicates that the destination of this route is a
|
||||
// "regular" address--one that neither belongs to this host, nor is a
|
||||
// broadcast/multicast/etc. address.
|
||||
RouteTypeUnicast
|
||||
// RouteTypeBroadcast indicates that the destination of this route is a
|
||||
// broadcast address.
|
||||
RouteTypeBroadcast
|
||||
// RouteTypeMulticast indicates that the destination of this route is a
|
||||
// multicast address.
|
||||
RouteTypeMulticast
|
||||
// RouteTypeOther indicates that the route is of some other valid type;
|
||||
// see the Sys field for the OS-provided route information to determine
|
||||
// the exact type.
|
||||
RouteTypeOther
|
||||
)
|
||||
|
||||
func (r RouteType) String() string {
|
||||
switch r {
|
||||
case RouteTypeUnspecified:
|
||||
return "unspecified"
|
||||
case RouteTypeLocal:
|
||||
return "local"
|
||||
case RouteTypeUnicast:
|
||||
return "unicast"
|
||||
case RouteTypeBroadcast:
|
||||
return "broadcast"
|
||||
case RouteTypeMulticast:
|
||||
return "multicast"
|
||||
case RouteTypeOther:
|
||||
return "other"
|
||||
default:
|
||||
return "invalid"
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type RouteEntryBSD struct {
|
||||
// GatewayInterface is the name of the interface specified as a gateway
|
||||
// for this route, if any.
|
||||
GatewayInterface string
|
||||
// GatewayIdx is the index of the interface specified as a gateway for
|
||||
// this route, if any.
|
||||
GatewayIdx int
|
||||
// GatewayAddr is the link-layer address of the gateway for this route,
|
||||
// if any.
|
||||
GatewayAddr string
|
||||
// Flags contains a string representation of common flags for this
|
||||
// route.
|
||||
Flags []string
|
||||
// RawFlags contains the raw flags that were returned by the operating
|
||||
// system for this route.
|
||||
RawFlags int
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface.
|
||||
func (r RouteEntryBSD) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
var pstart bool
|
||||
pr := func(format string, args ...any) {
|
||||
if pstart {
|
||||
fmt.Fprintf(w, ", "+format, args...)
|
||||
} else {
|
||||
fmt.Fprintf(w, format, args...)
|
||||
pstart = true
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteString("{")
|
||||
if r.GatewayInterface != "" {
|
||||
pr("GatewayInterface: %s", r.GatewayInterface)
|
||||
}
|
||||
if r.GatewayIdx > 0 {
|
||||
pr("GatewayIdx: %d", r.GatewayIdx)
|
||||
}
|
||||
if r.GatewayAddr != "" {
|
||||
pr("GatewayAddr: %s", r.GatewayAddr)
|
||||
}
|
||||
pr("Flags: %v", r.Flags)
|
||||
|
||||
w.WriteString("}")
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// ipFromRMAddr returns a netip.Addr converted from one of the
|
||||
// route.Inet{4,6}Addr types.
|
||||
func ipFromRMAddr(ifs map[int]interfaces.Interface, addr any) netip.Addr {
|
||||
switch v := addr.(type) {
|
||||
case *route.Inet4Addr:
|
||||
return netip.AddrFrom4(v.IP)
|
||||
|
||||
case *route.Inet6Addr:
|
||||
ip := netip.AddrFrom16(v.IP)
|
||||
if v.ZoneID != 0 {
|
||||
if iif, ok := ifs[v.ZoneID]; ok {
|
||||
ip = ip.WithZone(iif.Name)
|
||||
} else {
|
||||
ip = ip.WithZone(fmt.Sprint(v.ZoneID))
|
||||
}
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
// populateGateway populates gateway fields on a RouteEntry/RouteEntryBSD.
|
||||
func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]interfaces.Interface, addr any) {
|
||||
// If the address type has a valid IP, use that.
|
||||
if ip := ipFromRMAddr(ifs, addr); ip.IsValid() {
|
||||
re.Gateway = ip
|
||||
return
|
||||
}
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *route.LinkAddr:
|
||||
reSys.GatewayIdx = v.Index
|
||||
if iif, ok := ifs[v.Index]; ok {
|
||||
reSys.GatewayInterface = iif.Name
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i, x := range v.Addr {
|
||||
if i != 0 {
|
||||
sb.WriteByte(':')
|
||||
}
|
||||
fmt.Fprintf(&sb, "%02x", x)
|
||||
}
|
||||
reSys.GatewayAddr = sb.String()
|
||||
}
|
||||
}
|
||||
|
||||
// populateDestination populates the 'Dst' field on a RouteEntry based on the
|
||||
// RouteMessage's destination and netmask fields.
|
||||
func populateDestination(re *RouteEntry, ifs map[int]interfaces.Interface, rm *route.RouteMessage) {
|
||||
dst := rm.Addrs[unix.RTAX_DST]
|
||||
if dst == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ip := ipFromRMAddr(ifs, dst)
|
||||
if !ip.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
if ip.Is4() {
|
||||
re.Family = 4
|
||||
} else {
|
||||
re.Family = 6
|
||||
}
|
||||
re.Dst = RouteDestination{
|
||||
Prefix: netip.PrefixFrom(ip, 32), // default if nothing more specific
|
||||
}
|
||||
|
||||
// If the RTF_HOST flag is set, then this is a host route and there's
|
||||
// no netmask in this RouteMessage.
|
||||
if rm.Flags&unix.RTF_HOST != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// As above if there's no netmask in the list of addrs
|
||||
if len(rm.Addrs) < unix.RTAX_NETMASK || rm.Addrs[unix.RTAX_NETMASK] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
nm := ipFromRMAddr(ifs, rm.Addrs[unix.RTAX_NETMASK])
|
||||
if !ip.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// Count the number of bits in the netmask IP and use that to make our prefix.
|
||||
ones, _ /* bits */ := net.IPMask(nm.AsSlice()).Size()
|
||||
|
||||
// Print this ourselves instead of using netip.Prefix so that we don't
|
||||
// lose the zone (since netip.Prefix strips that).
|
||||
//
|
||||
// NOTE(andrew): this doesn't print the same values as the 'netstat' tool
|
||||
// for some addresses on macOS, and I have no idea why. Specifically,
|
||||
// 'netstat -rn' will show something like:
|
||||
// ff00::/8 ::1 UmCI lo0
|
||||
//
|
||||
// But we will get:
|
||||
// destination=ff00::/40 [...]
|
||||
//
|
||||
// The netmask that we get back from FetchRIB has 32 more bits in it
|
||||
// than netstat prints, but only for multicast routes.
|
||||
//
|
||||
// For consistency's sake, we're going to do the same here so that we
|
||||
// get the same values as netstat returns.
|
||||
if runtime.GOOS == "darwin" && ip.Is6() && ip.IsMulticast() && ones > 32 {
|
||||
ones -= 32
|
||||
}
|
||||
re.Dst = RouteDestination{
|
||||
Prefix: netip.PrefixFrom(ip, ones),
|
||||
Zone: ip.Zone(),
|
||||
}
|
||||
}
|
||||
|
||||
// routeEntryFromMsg returns a RouteEntry from a single route.Message
|
||||
// returned by the operating system.
|
||||
func routeEntryFromMsg(ifsByIdx map[int]interfaces.Interface, msg route.Message) (RouteEntry, bool) {
|
||||
rm, ok := msg.(*route.RouteMessage)
|
||||
if !ok {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
|
||||
// Ignore things that we don't understand
|
||||
if rm.Version < 3 || rm.Version > 5 {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
if rm.Type != rmExpectedType {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
if len(rm.Addrs) < unix.RTAX_GATEWAY {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
|
||||
if rm.Flags&skipFlags != 0 {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
|
||||
reSys := RouteEntryBSD{
|
||||
RawFlags: rm.Flags,
|
||||
}
|
||||
for fv, fs := range flags {
|
||||
if rm.Flags&fv == fv {
|
||||
reSys.Flags = append(reSys.Flags, fs)
|
||||
}
|
||||
}
|
||||
sort.Strings(reSys.Flags)
|
||||
|
||||
re := RouteEntry{}
|
||||
hasFlag := func(f int) bool { return rm.Flags&f != 0 }
|
||||
switch {
|
||||
case hasFlag(unix.RTF_LOCAL):
|
||||
re.Type = RouteTypeLocal
|
||||
case hasFlag(unix.RTF_BROADCAST):
|
||||
re.Type = RouteTypeBroadcast
|
||||
case hasFlag(unix.RTF_MULTICAST):
|
||||
re.Type = RouteTypeMulticast
|
||||
|
||||
// From the manpage: "host entry (net otherwise)"
|
||||
case !hasFlag(unix.RTF_HOST):
|
||||
re.Type = RouteTypeUnicast
|
||||
|
||||
default:
|
||||
re.Type = RouteTypeOther
|
||||
}
|
||||
populateDestination(&re, ifsByIdx, rm)
|
||||
if unix.RTAX_GATEWAY < len(rm.Addrs) {
|
||||
populateGateway(&re, &reSys, ifsByIdx, rm.Addrs[unix.RTAX_GATEWAY])
|
||||
}
|
||||
|
||||
if outif, ok := ifsByIdx[rm.Index]; ok {
|
||||
re.Interface = outif.Name
|
||||
}
|
||||
|
||||
re.Sys = reSys
|
||||
return re, true
|
||||
}
|
||||
|
||||
// Get returns route entries from the system route table, limited to at most
|
||||
// 'max' results.
|
||||
func Get(max int) ([]RouteEntry, error) {
|
||||
// Fetching the list of interfaces can race with fetching our route
|
||||
// table, but we do it anyway since it's helpful for debugging.
|
||||
ifs, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ifsByIdx := make(map[int]interfaces.Interface)
|
||||
for _, iif := range ifs {
|
||||
ifsByIdx[iif.Index] = iif
|
||||
}
|
||||
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, ribType, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs, err := route.ParseRIB(parseType, rib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []RouteEntry
|
||||
for _, m := range msgs {
|
||||
re, ok := routeEntryFromMsg(ifsByIdx, m)
|
||||
if ok {
|
||||
ret = append(ret, re)
|
||||
if len(ret) == max {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
func TestRouteEntryFromMsg(t *testing.T) {
|
||||
ifs := map[int]interfaces.Interface{
|
||||
1: {
|
||||
Interface: &net.Interface{
|
||||
Name: "iface0",
|
||||
},
|
||||
},
|
||||
2: {
|
||||
Interface: &net.Interface{
|
||||
Name: "tailscale0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ip4 := func(s string) *route.Inet4Addr {
|
||||
ip := netip.MustParseAddr(s)
|
||||
return &route.Inet4Addr{IP: ip.As4()}
|
||||
}
|
||||
ip6 := func(s string) *route.Inet6Addr {
|
||||
ip := netip.MustParseAddr(s)
|
||||
return &route.Inet6Addr{IP: ip.As16()}
|
||||
}
|
||||
ip6zone := func(s string, idx int) *route.Inet6Addr {
|
||||
ip := netip.MustParseAddr(s)
|
||||
return &route.Inet6Addr{IP: ip.As16(), ZoneID: idx}
|
||||
}
|
||||
link := func(idx int, addr string) *route.LinkAddr {
|
||||
if _, found := ifs[idx]; !found {
|
||||
panic("index not found")
|
||||
}
|
||||
|
||||
ret := &route.LinkAddr{
|
||||
Index: idx,
|
||||
}
|
||||
if addr != "" {
|
||||
ret.Addr = make([]byte, 6)
|
||||
fmt.Sscanf(addr, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
&ret.Addr[0],
|
||||
&ret.Addr[1],
|
||||
&ret.Addr[2],
|
||||
&ret.Addr[3],
|
||||
&ret.Addr[4],
|
||||
&ret.Addr[5],
|
||||
)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
msg *route.RouteMessage
|
||||
want RouteEntry
|
||||
fail bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "BasicIPv4",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
ip4("1.2.3.1"), // gateway
|
||||
ip4("255.255.255.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BasicIPv6",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6("fd7a:115c:a1e0::"), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ffff::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/48")},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6WithZone",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6zone("fe80::", 2), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ffff:ffff::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast, // TODO
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "tailscale0"},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6WithUnknownZone",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6zone("fe80::", 4), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ffff:ffff::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast, // TODO
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "4"},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultIPv4",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("0.0.0.0"), // dst
|
||||
ip4("1.2.3.4"), // gateway
|
||||
ip4("0.0.0.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: defaultRouteIPv4,
|
||||
Gateway: netip.MustParseAddr("1.2.3.4"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultIPv6",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6("0::"), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("0::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: defaultRouteIPv6,
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShortAddrs",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TailscaleIPv4",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("100.64.0.0"), // dst
|
||||
link(2, ""),
|
||||
ip4("255.192.0.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayInterface: "tailscale0",
|
||||
GatewayIdx: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Flags",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
ip4("1.2.3.1"), // gateway
|
||||
ip4("255.255.255.0"), // netmask
|
||||
},
|
||||
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Sys: RouteEntryBSD{
|
||||
Flags: []string{"gateway", "static", "up"},
|
||||
RawFlags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SkipNoAddrs",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{},
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "SkipBadVersion",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 1,
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "SkipBadType",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType + 1,
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "OutputIface",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Index: 1,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
|
||||
Interface: "iface0",
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GatewayMAC",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("100.64.0.0"), // dst
|
||||
link(1, "01:02:03:04:05:06"),
|
||||
ip4("255.192.0.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayAddr: "01:02:03:04:05:06",
|
||||
GatewayInterface: "iface0",
|
||||
GatewayIdx: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
testCases = append(testCases,
|
||||
testCase{
|
||||
name: "SkipFlags",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
ip4("1.2.3.1"), // gateway
|
||||
ip4("255.255.255.0"), // netmask
|
||||
},
|
||||
Flags: unix.RTF_UP | skipFlags,
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
testCase{
|
||||
name: "NetmaskAdjust",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Flags: unix.RTF_MULTICAST,
|
||||
Addrs: []route.Addr{
|
||||
ip6("ff00::"), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ff00::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeMulticast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("ff00::/8")},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{
|
||||
Flags: []string{"multicast"},
|
||||
RawFlags: unix.RTF_MULTICAST,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
re, ok := routeEntryFromMsg(ifs, tc.msg)
|
||||
if wantOk := !tc.fail; ok != wantOk {
|
||||
t.Fatalf("ok = %v; want %v", ok, wantOk)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(re, tc.want) {
|
||||
t.Fatalf("RouteEntry mismatch:\n got: %+v\nwant: %+v", re, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteEntryFormatting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
re RouteEntry
|
||||
want string
|
||||
}{
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.0/24")},
|
||||
Interface: "en0",
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayInterface: "en0",
|
||||
Flags: []string{"static", "up"},
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv4, Dst: 1.2.3.0/24, Interface: en0, Sys: {GatewayInterface: en0, Flags: [static up]}}`,
|
||||
},
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/24")},
|
||||
Interface: "en0",
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayIdx: 3,
|
||||
Flags: []string{"static", "up"},
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv6, Dst: fd7a:115c:a1e0::/24, Interface: en0, Sys: {GatewayIdx: 3, Flags: [static up]}}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got := fmt.Sprint(tc.re)
|
||||
if got != tc.want {
|
||||
t.Fatalf("RouteEntry.String() mismatch\n got: %q\nwant: %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRouteTable(t *testing.T) {
|
||||
routes, err := Get(1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Basic assertion: we have at least one 'default' route
|
||||
var (
|
||||
hasDefault bool
|
||||
)
|
||||
for _, route := range routes {
|
||||
if route.Dst == defaultRouteIPv4 || route.Dst == defaultRouteIPv6 {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
if !hasDefault {
|
||||
t.Errorf("expected at least one default route; routes=%v", routes)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package routetable
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
ribType = unix.NET_RT_DUMP2
|
||||
parseType = unix.NET_RT_IFLIST2
|
||||
rmExpectedType = unix.RTM_GET2
|
||||
|
||||
// Skip routes that were cloned from a parent
|
||||
skipFlags = unix.RTF_WASCLONED
|
||||
)
|
||||
|
||||
var flags = map[int]string{
|
||||
unix.RTF_BLACKHOLE: "blackhole",
|
||||
unix.RTF_BROADCAST: "broadcast",
|
||||
unix.RTF_GATEWAY: "gateway",
|
||||
unix.RTF_GLOBAL: "global",
|
||||
unix.RTF_HOST: "host",
|
||||
unix.RTF_IFSCOPE: "ifscope",
|
||||
unix.RTF_MULTICAST: "multicast",
|
||||
unix.RTF_REJECT: "reject",
|
||||
unix.RTF_ROUTER: "router",
|
||||
unix.RTF_STATIC: "static",
|
||||
unix.RTF_UP: "up",
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
ribType = unix.NET_RT_DUMP
|
||||
parseType = unix.NET_RT_IFLIST
|
||||
rmExpectedType = unix.RTM_GET
|
||||
|
||||
// Nothing to skip
|
||||
skipFlags = 0
|
||||
)
|
||||
|
||||
var flags = map[int]string{
|
||||
unix.RTF_BLACKHOLE: "blackhole",
|
||||
unix.RTF_BROADCAST: "broadcast",
|
||||
unix.RTF_GATEWAY: "gateway",
|
||||
unix.RTF_HOST: "host",
|
||||
unix.RTF_MULTICAST: "multicast",
|
||||
unix.RTF_REJECT: "reject",
|
||||
unix.RTF_STATIC: "static",
|
||||
unix.RTF_UP: "up",
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
"github.com/tailscale/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// RouteEntryLinux is the structure that makes up the Sys field of the
|
||||
// RouteEntry structure.
|
||||
type RouteEntryLinux struct {
|
||||
// Type is the raw type of the route.
|
||||
Type int
|
||||
// Table is the routing table index of this route.
|
||||
Table int
|
||||
// Src is the source of the route (if any).
|
||||
Src netip.Addr
|
||||
// Proto describes the source of the route--i.e. what caused this route
|
||||
// to be added to the route table.
|
||||
Proto netlink.RouteProtocol
|
||||
// Priority is the route's priority.
|
||||
Priority int
|
||||
// Scope is the route's scope.
|
||||
Scope int
|
||||
// InputInterfaceIdx is the input interface index.
|
||||
InputInterfaceIdx int
|
||||
// InputInterfaceName is the input interface name (if available).
|
||||
InputInterfaceName string
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface.
|
||||
func (r RouteEntryLinux) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
// TODO(andrew): should we skip printing anything if type is unicast?
|
||||
fmt.Fprintf(w, "{Type: %s", r.TypeName())
|
||||
|
||||
// Match 'ip route' behaviour when printing these fields
|
||||
if r.Table != unix.RT_TABLE_MAIN {
|
||||
fmt.Fprintf(w, ", Table: %s", r.TableName())
|
||||
}
|
||||
if r.Proto != unix.RTPROT_BOOT {
|
||||
fmt.Fprintf(w, ", Proto: %s", r.Proto)
|
||||
}
|
||||
|
||||
if r.Src.IsValid() {
|
||||
fmt.Fprintf(w, ", Src: %s", r.Src)
|
||||
}
|
||||
if r.Priority != 0 {
|
||||
fmt.Fprintf(w, ", Priority: %d", r.Priority)
|
||||
}
|
||||
if r.Scope != unix.RT_SCOPE_UNIVERSE {
|
||||
fmt.Fprintf(w, ", Scope: %s", r.ScopeName())
|
||||
}
|
||||
if r.InputInterfaceName != "" {
|
||||
fmt.Fprintf(w, ", InputInterfaceName: %s", r.InputInterfaceName)
|
||||
} else if r.InputInterfaceIdx != 0 {
|
||||
fmt.Fprintf(w, ", InputInterfaceIdx: %d", r.InputInterfaceIdx)
|
||||
}
|
||||
w.WriteString("}")
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// TypeName returns the string representation of this route's Type.
|
||||
func (r RouteEntryLinux) TypeName() string {
|
||||
switch r.Type {
|
||||
case unix.RTN_UNSPEC:
|
||||
return "none"
|
||||
case unix.RTN_UNICAST:
|
||||
return "unicast"
|
||||
case unix.RTN_LOCAL:
|
||||
return "local"
|
||||
case unix.RTN_BROADCAST:
|
||||
return "broadcast"
|
||||
case unix.RTN_ANYCAST:
|
||||
return "anycast"
|
||||
case unix.RTN_MULTICAST:
|
||||
return "multicast"
|
||||
case unix.RTN_BLACKHOLE:
|
||||
return "blackhole"
|
||||
case unix.RTN_UNREACHABLE:
|
||||
return "unreachable"
|
||||
case unix.RTN_PROHIBIT:
|
||||
return "prohibit"
|
||||
case unix.RTN_THROW:
|
||||
return "throw"
|
||||
case unix.RTN_NAT:
|
||||
return "nat"
|
||||
case unix.RTN_XRESOLVE:
|
||||
return "xresolve"
|
||||
default:
|
||||
return strconv.Itoa(r.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the string representation of this route's Table.
|
||||
func (r RouteEntryLinux) TableName() string {
|
||||
switch r.Table {
|
||||
case unix.RT_TABLE_DEFAULT:
|
||||
return "default"
|
||||
case unix.RT_TABLE_MAIN:
|
||||
return "main"
|
||||
case unix.RT_TABLE_LOCAL:
|
||||
return "local"
|
||||
default:
|
||||
return strconv.Itoa(r.Table)
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeName returns the string representation of this route's Scope.
|
||||
func (r RouteEntryLinux) ScopeName() string {
|
||||
switch r.Scope {
|
||||
case unix.RT_SCOPE_UNIVERSE:
|
||||
return "global"
|
||||
case unix.RT_SCOPE_NOWHERE:
|
||||
return "nowhere"
|
||||
case unix.RT_SCOPE_HOST:
|
||||
return "host"
|
||||
case unix.RT_SCOPE_LINK:
|
||||
return "link"
|
||||
case unix.RT_SCOPE_SITE:
|
||||
return "site"
|
||||
default:
|
||||
return strconv.Itoa(r.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns route entries from the system route table, limited to at most
|
||||
// max results.
|
||||
func Get(max int) ([]RouteEntry, error) {
|
||||
// Fetching the list of interfaces can race with fetching our route
|
||||
// table, but we do it anyway since it's helpful for debugging.
|
||||
ifs, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ifsByIdx := make(map[int]interfaces.Interface)
|
||||
for _, iif := range ifs {
|
||||
ifsByIdx[iif.Index] = iif
|
||||
}
|
||||
|
||||
filter := &netlink.Route{}
|
||||
routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, filter, netlink.RT_FILTER_TABLE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []RouteEntry
|
||||
for _, route := range routes {
|
||||
if route.Family != netlink.FAMILY_V4 && route.Family != netlink.FAMILY_V6 {
|
||||
continue
|
||||
}
|
||||
|
||||
re := RouteEntry{}
|
||||
if route.Family == netlink.FAMILY_V4 {
|
||||
re.Family = 4
|
||||
} else {
|
||||
re.Family = 6
|
||||
}
|
||||
switch route.Type {
|
||||
case unix.RTN_UNSPEC:
|
||||
re.Type = RouteTypeUnspecified
|
||||
case unix.RTN_UNICAST:
|
||||
re.Type = RouteTypeUnicast
|
||||
case unix.RTN_LOCAL:
|
||||
re.Type = RouteTypeLocal
|
||||
case unix.RTN_BROADCAST:
|
||||
re.Type = RouteTypeBroadcast
|
||||
case unix.RTN_MULTICAST:
|
||||
re.Type = RouteTypeMulticast
|
||||
default:
|
||||
re.Type = RouteTypeOther
|
||||
}
|
||||
if route.Dst != nil {
|
||||
if d, ok := netaddr.FromStdIPNet(route.Dst); ok {
|
||||
re.Dst = RouteDestination{Prefix: d}
|
||||
}
|
||||
} else if route.Family == netlink.FAMILY_V4 {
|
||||
re.Dst = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
|
||||
} else {
|
||||
re.Dst = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
|
||||
}
|
||||
if gw := route.Gw; gw != nil {
|
||||
if gwa, ok := netip.AddrFromSlice(gw); ok {
|
||||
re.Gateway = gwa
|
||||
}
|
||||
}
|
||||
if outif, ok := ifsByIdx[route.LinkIndex]; ok {
|
||||
re.Interface = outif.Name
|
||||
} else if route.LinkIndex > 0 {
|
||||
re.Interface = fmt.Sprintf("link#%d", route.LinkIndex)
|
||||
}
|
||||
reSys := RouteEntryLinux{
|
||||
Type: route.Type,
|
||||
Table: route.Table,
|
||||
Proto: route.Protocol,
|
||||
Priority: route.Priority,
|
||||
Scope: int(route.Scope),
|
||||
InputInterfaceIdx: route.ILinkIndex,
|
||||
}
|
||||
if src, ok := netip.AddrFromSlice(route.Src); ok {
|
||||
reSys.Src = src
|
||||
}
|
||||
if iif, ok := ifsByIdx[route.ILinkIndex]; ok {
|
||||
reSys.InputInterfaceName = iif.Name
|
||||
}
|
||||
|
||||
re.Sys = reSys
|
||||
ret = append(ret, re)
|
||||
|
||||
// Stop after we've reached the maximum number of routes
|
||||
if len(ret) == max {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestGetRouteTable(t *testing.T) {
|
||||
routes, err := Get(1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Basic assertion: we have at least one 'default' route in the main table
|
||||
var (
|
||||
hasDefault bool
|
||||
)
|
||||
for _, route := range routes {
|
||||
if route.Dst == defaultRouteIPv4 && route.Sys.(RouteEntryLinux).Table == unix.RT_TABLE_MAIN {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
if !hasDefault {
|
||||
t.Errorf("expected at least one default route; routes=%v", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteEntryFormatting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
re RouteEntry
|
||||
want string
|
||||
}{
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeMulticast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Interface: "tailscale0",
|
||||
Sys: RouteEntryLinux{
|
||||
Type: unix.RTN_UNICAST,
|
||||
Table: 52,
|
||||
Proto: unix.RTPROT_STATIC,
|
||||
Src: netip.MustParseAddr("1.2.3.4"),
|
||||
Priority: 555,
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv4, Type: multicast, Dst: 100.64.0.0/10, Gateway: 1.2.3.1, Interface: tailscale0, Sys: {Type: unicast, Table: 52, Proto: static, Src: 1.2.3.4, Priority: 555}}`,
|
||||
},
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.0/24")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Sys: RouteEntryLinux{
|
||||
Type: unix.RTN_UNICAST,
|
||||
Table: unix.RT_TABLE_MAIN,
|
||||
Proto: unix.RTPROT_BOOT,
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv4, Dst: 1.2.3.0/24, Gateway: 1.2.3.1, Sys: {Type: unicast}}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got := fmt.Sprint(tc.re)
|
||||
if got != tc.want {
|
||||
t.Fatalf("RouteEntry.String() = %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !linux && !darwin && !freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var errUnsupported = errors.New("cannot get route table on platform " + runtime.GOOS)
|
||||
|
||||
func Get(max int) ([]RouteEntry, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
@@ -81,8 +81,7 @@ type CapabilityVersion int
|
||||
// - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
|
||||
// - 43: 2022-09-21: clients can return usernames for SSH
|
||||
// - 44: 2022-09-22: MapResponse.ControlDialPlan
|
||||
// - 45: 2022-09-26: c2n /debug/{goroutines,prefs,metrics}
|
||||
const CurrentCapabilityVersion CapabilityVersion = 45
|
||||
const CurrentCapabilityVersion CapabilityVersion = 44
|
||||
|
||||
type StableID string
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ import (
|
||||
// TKAInitBeginRequest submits a genesis AUM to seed the creation of the
|
||||
// tailnet's key authority.
|
||||
type TKAInitBeginRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
// NodeID is the node of the initiating client.
|
||||
// It must match the machine key being used to communicate over noise.
|
||||
NodeID NodeID
|
||||
|
||||
// GenesisAUM is the initial (genesis) AUM that the node generated
|
||||
// to bootstrap tailnet key authority state.
|
||||
@@ -60,11 +58,8 @@ type TKAInitBeginResponse struct {
|
||||
// This RPC finalizes initialization of the tailnet key authority
|
||||
// by submitting node-key signatures for all existing nodes.
|
||||
type TKAInitFinishRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
// NodeID is the node ID of the initiating client.
|
||||
NodeID NodeID
|
||||
|
||||
// Signatures are serialized tka.NodeKeySignatures for all nodes
|
||||
// in the tailnet.
|
||||
@@ -106,12 +101,8 @@ type TKAInfo struct {
|
||||
// TKABootstrapRequest is sent by a node to get information necessary for
|
||||
// enabling or disabling the tailnet key authority.
|
||||
type TKABootstrapRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// NodeID is the node ID of the initiating client.
|
||||
NodeID NodeID
|
||||
// Head represents the node's head AUMHash (tka.Authority.Head), if
|
||||
// network lock is enabled.
|
||||
Head string
|
||||
@@ -131,12 +122,8 @@ type TKABootstrapResponse struct {
|
||||
// state (TKA). Values of type tka.AUMHash are encoded as strings in their
|
||||
// MarshalText form.
|
||||
type TKASyncOfferRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// NodeID is the node ID of the initiating client.
|
||||
NodeID NodeID
|
||||
// Head represents the node's head AUMHash (tka.Authority.Head). This
|
||||
// corresponds to tka.SyncOffer.Head.
|
||||
Head string
|
||||
@@ -164,12 +151,8 @@ type TKASyncOfferResponse struct {
|
||||
// TKASyncSendRequest encodes AUMs that a node believes the control plane
|
||||
// is missing.
|
||||
type TKASyncSendRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// NodeID is the node ID of the initiating client.
|
||||
NodeID NodeID
|
||||
// MissingAUMs encodes AUMs that the node believes the control plane
|
||||
// is missing.
|
||||
MissingAUMs []tkatype.MarshaledAUM
|
||||
|
||||
@@ -50,6 +50,7 @@ func (h AUMHash) IsZero() bool {
|
||||
return h == (AUMHash{})
|
||||
}
|
||||
|
||||
|
||||
// AUMKind describes valid AUM types.
|
||||
type AUMKind uint8
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ package tka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
@@ -93,62 +92,8 @@ func (b *UpdateBuilder) SetKeyMeta(keyID tkatype.KeyID, meta map[string]string)
|
||||
return b.mkUpdate(AUM{MessageKind: AUMUpdateKey, Meta: meta, KeyID: keyID})
|
||||
}
|
||||
|
||||
func (b *UpdateBuilder) generateCheckpoint() error {
|
||||
// Compute the checkpoint state.
|
||||
state := b.a.state
|
||||
for i, update := range b.out {
|
||||
var err error
|
||||
if state, err = state.applyVerifiedAUM(update); err != nil {
|
||||
return fmt.Errorf("applying update %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Checkpoints cant specify a parent AUM.
|
||||
state.LastAUMHash = nil
|
||||
return b.mkUpdate(AUM{MessageKind: AUMCheckpoint, State: &state})
|
||||
}
|
||||
|
||||
// checkpointEvery sets how often a checkpoint AUM should be generated.
|
||||
const checkpointEvery = 50
|
||||
|
||||
// Finalize returns the set of update message to actuate the update.
|
||||
func (b *UpdateBuilder) Finalize(storage Chonk) ([]AUM, error) {
|
||||
var (
|
||||
needCheckpoint bool = true
|
||||
cursor AUMHash = b.a.Head()
|
||||
)
|
||||
for i := len(b.out); i < checkpointEvery; i++ {
|
||||
aum, err := storage.AUM(cursor)
|
||||
if err != nil {
|
||||
if err == os.ErrNotExist {
|
||||
// The available chain is shorter than the interval to checkpoint at.
|
||||
needCheckpoint = false
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("reading AUM: %v", err)
|
||||
}
|
||||
|
||||
if aum.MessageKind == AUMCheckpoint {
|
||||
needCheckpoint = false
|
||||
break
|
||||
}
|
||||
|
||||
parent, hasParent := aum.Parent()
|
||||
if !hasParent {
|
||||
// We've hit the genesis update, so the chain is shorter than the interval to checkpoint at.
|
||||
needCheckpoint = false
|
||||
break
|
||||
}
|
||||
cursor = parent
|
||||
}
|
||||
|
||||
if needCheckpoint {
|
||||
if err := b.generateCheckpoint(); err != nil {
|
||||
return nil, fmt.Errorf("generating checkpoint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check no AUMs were applied in the meantime
|
||||
func (b *UpdateBuilder) Finalize() ([]AUM, error) {
|
||||
if len(b.out) > 0 {
|
||||
if parent, _ := b.out[0].Parent(); parent != b.a.Head() {
|
||||
return nil, fmt.Errorf("updates no longer apply to head: based on %x but head is %x", parent, b.a.Head())
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
|
||||
if err := b.AddKey(key2); err != nil {
|
||||
t.Fatalf("AddKey(%v) failed: %v", key2, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
|
||||
if err := b.RemoveKey(key2.ID()); err != nil {
|
||||
t.Fatalf("RemoveKey(%v) failed: %v", key2, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
|
||||
if err := b.SetKeyVote(key.ID(), 5); err != nil {
|
||||
t.Fatalf("SetKeyVote(%v) failed: %v", key.ID(), err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
|
||||
if err := b.SetKeyMeta(key.ID(), map[string]string{"b": "c"}); err != nil {
|
||||
t.Fatalf("SetKeyMeta(%v) failed: %v", key, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -191,7 +191,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
if err := b.RemoveKey(key.ID()); err != nil {
|
||||
t.Fatalf("RemoveKey(%v) failed: %v", key, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -212,60 +212,3 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
t.Errorf("GetKey(key).err = %v, want %v", err, ErrNoSuchKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorityBuilderCheckpointsAfterXUpdates(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{Kind: Key25519, Public: pub, Votes: 2}
|
||||
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i <= checkpointEvery; i++ {
|
||||
pub2, _ := testingKey25519(t, int64(i+2))
|
||||
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
|
||||
|
||||
b := a.NewUpdater(signer25519(priv))
|
||||
if err := b.AddKey(key2); err != nil {
|
||||
t.Fatalf("AddKey(%v) failed: %v", key2, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the new key is there.
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
if _, err := a.state.GetKey(key2.ID()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantKind := AUMAddKey
|
||||
if i == checkpointEvery-1 { // Genesis + 49 updates == 50 (the value of checkpointEvery)
|
||||
wantKind = AUMCheckpoint
|
||||
}
|
||||
lastAUM, err := storage.AUM(a.Head())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lastAUM.MessageKind != wantKind {
|
||||
t.Errorf("[%d] HeadAUM.MessageKind = %v, want %v", i, lastAUM.MessageKind, wantKind)
|
||||
}
|
||||
}
|
||||
|
||||
// Try starting an authority just based on storage.
|
||||
a2, err := Open(storage)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open from stored AUMs: %v", err)
|
||||
}
|
||||
if a.Head() != a2.Head() {
|
||||
t.Errorf("stored and computed HEAD differ: got %v, want %v", a2.Head(), a.Head())
|
||||
}
|
||||
}
|
||||
|
||||
17
tka/tka.go
17
tka/tka.go
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
@@ -182,18 +181,6 @@ func advanceByPrimary(state State, candidates []AUM) (next *AUM, out State, err
|
||||
}
|
||||
|
||||
aum := pickNextAUM(state, candidates)
|
||||
|
||||
// TODO(tom): Remove this before GA, this is just a correctness check during implementation.
|
||||
// Post-GA, we want clients to not error if they dont recognize additional fields in State.
|
||||
if aum.MessageKind == AUMCheckpoint {
|
||||
dupe := state
|
||||
dupe.LastAUMHash = nil
|
||||
// aum.State is non-nil (see aum.StaticValidate).
|
||||
if !reflect.DeepEqual(dupe, *aum.State) {
|
||||
return nil, State{}, errors.New("checkpoint includes changes not represented in earlier AUMs")
|
||||
}
|
||||
}
|
||||
|
||||
if state, err = state.applyVerifiedAUM(aum); err != nil {
|
||||
return nil, State{}, fmt.Errorf("advancing state: %v", err)
|
||||
}
|
||||
@@ -257,6 +244,10 @@ func fastForward(storage Chonk, maxIter int, startState State, done func(curAUM
|
||||
|
||||
// computeStateAt returns the State at wantHash.
|
||||
func computeStateAt(storage Chonk, maxIter int, wantHash AUMHash) (State, error) {
|
||||
// TODO(tom): This is going to get expensive for really long
|
||||
// chains. We should make nodes emit a checkpoint every
|
||||
// X updates or something.
|
||||
|
||||
topAUM, err := storage.AUM(wantHash)
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
|
||||
@@ -138,8 +138,6 @@ var rateFree = []string{
|
||||
"SetPrefs: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
// debug messages printed by 'tailscale bugreport'
|
||||
"diag: ",
|
||||
}
|
||||
|
||||
// RateLimitedFn is a wrapper for RateLimitedFnWithClock that includes the
|
||||
|
||||
@@ -106,20 +106,9 @@ func (s1 *Sum) xor(s2 Sum) {
|
||||
}
|
||||
|
||||
func (s Sum) String() string {
|
||||
// Note: if we change this, keep in sync with AppendTo
|
||||
return hex.EncodeToString(s.sum[:])
|
||||
}
|
||||
|
||||
// AppendTo appends the string encoding of this sum (as returned by the String
|
||||
// method) to the provided byte slice and returns the extended buffer.
|
||||
func (s Sum) AppendTo(b []byte) []byte {
|
||||
// TODO: switch to upstream implementation if accepted:
|
||||
// https://github.com/golang/go/issues/53693
|
||||
var lb [len(s.sum) * 2]byte
|
||||
hex.Encode(lb[:], s.sum[:])
|
||||
return append(b, lb[:]...)
|
||||
}
|
||||
|
||||
var (
|
||||
seedOnce sync.Once
|
||||
seed uint64
|
||||
|
||||
@@ -1050,25 +1050,3 @@ func FuzzAddr(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendTo(t *testing.T) {
|
||||
v := getVal()
|
||||
h := Hash(v)
|
||||
sum := h.AppendTo(nil)
|
||||
|
||||
if s := h.String(); s != string(sum) {
|
||||
t.Errorf("hash sum mismatch; h.String()=%q h.AppendTo()=%q", s, string(sum))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendTo(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
v := getVal()
|
||||
h := Hash(v)
|
||||
|
||||
hashBuf := make([]byte, 0, 100)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hashBuf = h.AppendTo(hashBuf[:0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The goroutines package contains utilities for getting active goroutines.
|
||||
package goroutines
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ScrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func ScrubbedGoroutineDump() []byte {
|
||||
var buf []byte
|
||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||
// to minimize the risk that we blow past our iOS memory limit.
|
||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
// It fit.
|
||||
break
|
||||
}
|
||||
}
|
||||
return scrubHex(buf)
|
||||
}
|
||||
|
||||
func scrubHex(buf []byte) []byte {
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
|
||||
foreachHexAddress(buf, func(in []byte) {
|
||||
if string(in) == "0x0" {
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
var ohx = []byte("0x")
|
||||
|
||||
// foreachHexAddress calls f with each subslice of b that matches
|
||||
// regexp `0x[0-9a-f]*`.
|
||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||
for len(b) > 0 {
|
||||
i := bytes.Index(b, ohx)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
b = b[i:]
|
||||
hx := hexPrefix(b)
|
||||
f(hx)
|
||||
b = b[len(hx):]
|
||||
}
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
if !isHexByte(c) {
|
||||
return b[:i]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isHexByte(b byte) bool {
|
||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||
}
|
||||
@@ -987,19 +987,16 @@ func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) {
|
||||
}
|
||||
dstAddr, ok := ipPortOfNetstackAddr(sess.LocalAddress, sess.LocalPort)
|
||||
if !ok {
|
||||
ep.Close()
|
||||
return
|
||||
}
|
||||
srcAddr, ok := ipPortOfNetstackAddr(sess.RemoteAddress, sess.RemotePort)
|
||||
if !ok {
|
||||
ep.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle magicDNS traffic (via UDP) here.
|
||||
if dst := dstAddr.Addr(); dst == magicDNSIP || dst == magicDNSIPv6 {
|
||||
if dstAddr.Port() != 53 {
|
||||
ep.Close()
|
||||
return // Only MagicDNS traffic runs on the service IPs for now.
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user