Compare commits
79 Commits
bradfitz/t
...
v1.26.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b81baa7d3 | ||
|
|
32f26a723b | ||
|
|
c1795c6af9 | ||
|
|
4ae7eff7c2 | ||
|
|
d76cce4ee7 | ||
|
|
7968313a33 | ||
|
|
4e6a465d8c | ||
|
|
89ac48af9d | ||
|
|
9fc6551b4e | ||
|
|
db83926121 | ||
|
|
27a1ad6a70 | ||
|
|
4007601f73 | ||
|
|
3b55bf9306 | ||
|
|
bf2fa7b184 | ||
|
|
0d972678e7 | ||
|
|
0df3b76c25 | ||
|
|
c6ac82e3a6 | ||
|
|
0687195bee | ||
|
|
fbc079d82d | ||
|
|
2bac8b6013 | ||
|
|
03e3e6abcd | ||
|
|
a9b4bf1535 | ||
|
|
43f9c25fd2 | ||
|
|
c980bf01be | ||
|
|
a9f32656f5 | ||
|
|
80157f3f37 | ||
|
|
69b535c01f | ||
|
|
e428bba7a3 | ||
|
|
67325d334e | ||
|
|
1336fb740b | ||
|
|
81487169f0 | ||
|
|
3a926348a4 | ||
|
|
2a61261a5a | ||
|
|
928530a112 | ||
|
|
4d85cf586b | ||
|
|
575aacb1e2 | ||
|
|
7cd8c3e839 | ||
|
|
760740905e | ||
|
|
02e580c1d2 | ||
|
|
2903d42921 | ||
|
|
b005b79236 | ||
|
|
c16271fb46 | ||
|
|
0f95eaa8bb | ||
|
|
3f686688a6 | ||
|
|
c163b2a3f1 | ||
|
|
fc5839864b | ||
|
|
c81be7b899 | ||
|
|
36af49ae7f | ||
|
|
bded712e58 | ||
|
|
7cfc6130e5 | ||
|
|
eda647cb47 | ||
|
|
cc91a05686 | ||
|
|
3222bce02d | ||
|
|
acfe5bd33b | ||
|
|
ec4c49a338 | ||
|
|
53f6c3f9f2 | ||
|
|
48d43134d7 | ||
|
|
afb3f62b01 | ||
|
|
da601c23e1 | ||
|
|
e1c1d47991 | ||
|
|
9343967317 | ||
|
|
c48513b2be | ||
|
|
561f7be434 | ||
|
|
dd5548771e | ||
|
|
86069874c9 | ||
|
|
87b44aa311 | ||
|
|
4bb7440094 | ||
|
|
6dae9e47f9 | ||
|
|
4c75605e23 | ||
|
|
395cb588b6 | ||
|
|
d04afc697c | ||
|
|
5cd56fe8d5 | ||
|
|
a253057fc3 | ||
|
|
741ae9956e | ||
|
|
9f3ad40707 | ||
|
|
fd99c54e10 | ||
|
|
679415f3a8 | ||
|
|
c4e9739251 | ||
|
|
e409e59a54 |
47
.github/workflows/cross-wasm.yml
vendored
Normal file
47
.github/workflows/cross-wasm.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Wasm-Cross
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Wasm build CLI and client modules
|
||||
env:
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
run: go build ./cmd/tailscale/cli ./ipn/... ./net/... ./safesocket ./types/... ./wgengine/...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
@@ -1 +1 @@
|
||||
1.25.0
|
||||
1.26.1
|
||||
|
||||
16
api.md
16
api.md
@@ -815,11 +815,15 @@ Supply the tailnet in the path.
|
||||
`capabilities` - A mapping of resources to permissible actions.
|
||||
```
|
||||
{
|
||||
"capabilities": {
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
"ephemeral": false,
|
||||
"preauthorized": false,
|
||||
"tags": [
|
||||
"tag:example"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,11 +843,13 @@ The full key can no longer be retrieved by the server.
|
||||
|
||||
```
|
||||
echo '{
|
||||
"capabilities": {
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
"ephemeral": false,
|
||||
"preauthorized": false,
|
||||
"tags": [ "tag:example" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -859,7 +865,7 @@ Response:
|
||||
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
|
||||
"created": "2021-12-09T23:22:39Z",
|
||||
"expires": "2022-03-09T23:22:39Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false, "preauthorized": false, "tags": [ "tag:example" ]}}}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
TAGS="${TAGS:-${DEFAULT_TAGS}}"
|
||||
BASE="${BASE:-${DEFAULT_BASE}}"
|
||||
|
||||
go run github.com/tailscale/mkctr@latest \
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="\
|
||||
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
|
||||
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
|
||||
@@ -40,7 +40,9 @@ go run github.com/tailscale/mkctr@latest \
|
||||
-X tailscale.com/version.Long=${VERSION_LONG} \
|
||||
-X tailscale.com/version.Short=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
|
||||
--files="docs/k8s/run.sh:/tailscale/run.sh" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}"
|
||||
--push="${PUSH}" \
|
||||
/bin/sh /tailscale/run.sh
|
||||
|
||||
@@ -22,13 +22,11 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"tailscale.com/util/codegen"
|
||||
)
|
||||
|
||||
var (
|
||||
flagTypes = flag.String("type", "", "comma-separated list of types; required")
|
||||
flagOutput = flag.String("output", "", "output file; required")
|
||||
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
|
||||
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
||||
)
|
||||
@@ -43,30 +41,18 @@ func main() {
|
||||
}
|
||||
typeNames := strings.Split(*flagTypes, ",")
|
||||
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedName,
|
||||
Tests: false,
|
||||
}
|
||||
if *flagBuildTags != "" {
|
||||
cfg.BuildFlags = []string{"-tags=" + *flagBuildTags}
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, ".")
|
||||
pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(pkgs) != 1 {
|
||||
log.Fatalf("wrong number of packages: %d", len(pkgs))
|
||||
}
|
||||
pkg := pkgs[0]
|
||||
it := codegen.NewImportTracker(pkg.Types)
|
||||
buf := new(bytes.Buffer)
|
||||
imports := make(map[string]struct{})
|
||||
namedTypes := codegen.NamedTypes(pkg)
|
||||
for _, typeName := range typeNames {
|
||||
typ, ok := namedTypes[typeName]
|
||||
if !ok {
|
||||
log.Fatalf("could not find type %s", typeName)
|
||||
}
|
||||
gen(buf, imports, typ, pkg.Types)
|
||||
gen(buf, it, typ)
|
||||
}
|
||||
|
||||
w := func(format string, args ...any) {
|
||||
@@ -93,62 +79,13 @@ func main() {
|
||||
w(" return false")
|
||||
w("}")
|
||||
}
|
||||
|
||||
contents := new(bytes.Buffer)
|
||||
var flagArgs []string
|
||||
if *flagTypes != "" {
|
||||
flagArgs = append(flagArgs, "-type="+*flagTypes)
|
||||
}
|
||||
if *flagOutput != "" {
|
||||
flagArgs = append(flagArgs, "-output="+*flagOutput)
|
||||
}
|
||||
if *flagBuildTags != "" {
|
||||
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
|
||||
}
|
||||
if *flagCloneFunc {
|
||||
flagArgs = append(flagArgs, "-clonefunc")
|
||||
}
|
||||
fmt.Fprintf(contents, header, strings.Join(flagArgs, " "), pkg.Name)
|
||||
fmt.Fprintf(contents, "import (\n")
|
||||
for s := range imports {
|
||||
fmt.Fprintf(contents, "\t%q\n", s)
|
||||
}
|
||||
fmt.Fprintf(contents, ")\n\n")
|
||||
contents.Write(buf.Bytes())
|
||||
|
||||
output := *flagOutput
|
||||
if output == "" {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := codegen.WriteFormatted(contents.Bytes(), output); err != nil {
|
||||
cloneOutput := pkg.Name + "_clone.go"
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const header = `// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//` + `go:generate` + ` go run tailscale.com/cmd/cloner %s
|
||||
|
||||
package %s
|
||||
|
||||
`
|
||||
|
||||
func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisPkg *types.Package) {
|
||||
pkgQual := func(pkg *types.Package) string {
|
||||
if thisPkg == pkg {
|
||||
return ""
|
||||
}
|
||||
imports[pkg.Path()] = struct{}{}
|
||||
return pkg.Name()
|
||||
}
|
||||
importedName := func(t types.Type) string {
|
||||
return types.TypeString(t, pkgQual)
|
||||
}
|
||||
|
||||
func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
t, ok := typ.Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
return
|
||||
@@ -169,11 +106,11 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
|
||||
for i := 0; i < t.NumFields(); i++ {
|
||||
fname := t.Field(i).Name()
|
||||
ft := t.Field(i).Type()
|
||||
if !codegen.ContainsPointers(ft) {
|
||||
if !codegen.ContainsPointers(ft) || codegen.HasNoClone(t.Tag(i)) {
|
||||
continue
|
||||
}
|
||||
if named, _ := ft.(*types.Named); named != nil {
|
||||
if isViewType(ft) {
|
||||
if codegen.IsViewType(ft) {
|
||||
writef("dst.%s = src.%s", fname, fname)
|
||||
continue
|
||||
}
|
||||
@@ -185,11 +122,16 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
|
||||
switch ft := ft.Underlying().(type) {
|
||||
case *types.Slice:
|
||||
if codegen.ContainsPointers(ft.Elem()) {
|
||||
n := importedName(ft.Elem())
|
||||
n := it.QualifiedName(ft.Elem())
|
||||
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
|
||||
writef("for i := range dst.%s {", fname)
|
||||
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
|
||||
if _, isBasic := ptr.Elem().Underlying().(*types.Basic); isBasic {
|
||||
writef("\tx := *src.%s[i]", fname)
|
||||
writef("\tdst.%s[i] = &x", fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
} else {
|
||||
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
@@ -202,7 +144,7 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
n := importedName(ft.Elem())
|
||||
n := it.QualifiedName(ft.Elem())
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = new(%s)", fname, n)
|
||||
writef("\t*dst.%s = *src.%s", fname, fname)
|
||||
@@ -212,9 +154,9 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
|
||||
writef("}")
|
||||
case *types.Map:
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(ft.Elem()))
|
||||
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
|
||||
n := importedName(sliceType.Elem())
|
||||
n := it.QualifiedName(sliceType.Elem())
|
||||
writef("\tfor k := range src.%s {", fname)
|
||||
// use zero-length slice instead of nil to ensure
|
||||
// the key is always copied.
|
||||
@@ -237,20 +179,10 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
|
||||
writef("return dst")
|
||||
fmt.Fprintf(buf, "}\n\n")
|
||||
|
||||
buf.Write(codegen.AssertStructUnchanged(t, thisPkg, name, "Clone", imports))
|
||||
}
|
||||
|
||||
func isViewType(typ types.Type) bool {
|
||||
t, ok := typ.Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if t.NumFields() != 1 {
|
||||
return false
|
||||
}
|
||||
return t.Field(0).Name() == "ж"
|
||||
buf.Write(codegen.AssertStructUnchanged(t, name, "Clone", it))
|
||||
}
|
||||
|
||||
// hasBasicUnderlying reports true when typ.Underlying() is a slice or a map.
|
||||
func hasBasicUnderlying(typ types.Type) bool {
|
||||
switch typ.Underlying().(type) {
|
||||
case *types.Slice, *types.Map:
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/wsconn"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -23,11 +25,14 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -118,6 +123,17 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runVia,
|
||||
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
},
|
||||
{
|
||||
Name: "ts2021",
|
||||
Exec: runTS2021,
|
||||
ShortHelp: "debug ts2021 protocol connectivity",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("ts2021")
|
||||
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
|
||||
fs.IntVar(&ts2021Args.version, "version", int(tailcfg.CurrentCapabilityVersion), "protocol version")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -425,3 +441,67 @@ func runVia(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ts2021Args struct {
|
||||
host string // "controlplane.tailscale.com"
|
||||
version int // 27 or whatever
|
||||
}
|
||||
|
||||
func runTS2021(ctx context.Context, args []string) error {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
|
||||
machinePrivate := key.NewMachine()
|
||||
var dialer net.Dialer
|
||||
|
||||
var keys struct {
|
||||
PublicKey key.MachinePublic
|
||||
}
|
||||
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
|
||||
log.Printf("Fetching keys from %s ...", keysURL)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", keysURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Do: %v", err)
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
log.Printf("Status: %v", res.Status)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&keys); err != nil {
|
||||
log.Printf("JSON: %v", err)
|
||||
return fmt.Errorf("decoding /keys JSON: %w", err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
dialFunc := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
log.Printf("Dial(%q, %q) ...", network, address)
|
||||
c, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
log.Printf("Dial(%q, %q) = %v", network, address, err)
|
||||
} else {
|
||||
log.Printf("Dial(%q, %q) = %v / %v", network, address, c.LocalAddr(), c.RemoteAddr())
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
conn, err := controlhttp.Dial(ctx, net.JoinHostPort(ts2021Args.host, "80"), machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
log.Printf("controlhttp.Dial = %p, %v", conn, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("did noise handshake")
|
||||
|
||||
gotPeer := conn.Peer()
|
||||
if gotPeer != keys.PublicKey {
|
||||
log.Printf("peer = %v, want %v", gotPeer, keys.PublicKey)
|
||||
return errors.New("key mismatch")
|
||||
}
|
||||
|
||||
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
@@ -17,7 +18,14 @@ var downCmd = &ffcli.Command{
|
||||
ShortUsage: "down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
Exec: runDown,
|
||||
FlagSet: newDownFlagSet(),
|
||||
}
|
||||
|
||||
func newDownFlagSet() *flag.FlagSet {
|
||||
downf := newFlagSet("down")
|
||||
registerAcceptRiskFlag(downf)
|
||||
return downf
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
@@ -25,6 +33,12 @@ func runDown(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if isSSHOverTailscale() {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching current status: %w", err)
|
||||
|
||||
@@ -51,6 +51,7 @@ relay node.
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)")
|
||||
fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)")
|
||||
fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
@@ -63,6 +64,7 @@ var pingArgs struct {
|
||||
verbose bool
|
||||
tsmp bool
|
||||
icmp bool
|
||||
peerAPI bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
@@ -73,6 +75,9 @@ func pingType() tailcfg.PingType {
|
||||
if pingArgs.icmp {
|
||||
return tailcfg.PingICMP
|
||||
}
|
||||
if pingArgs.peerAPI {
|
||||
return tailcfg.PingPeerAPI
|
||||
}
|
||||
return tailcfg.PingDisco
|
||||
}
|
||||
|
||||
@@ -137,6 +142,10 @@ func runPing(ctx context.Context, args []string) error {
|
||||
// For now just say which protocol it used.
|
||||
via = string(pingType())
|
||||
}
|
||||
if pingArgs.peerAPI {
|
||||
printf("hit peerapi of %s (%s) at %s in %s\n", pr.NodeIP, pr.NodeName, pr.PeerAPIURL, latency)
|
||||
return nil
|
||||
}
|
||||
anyPong = true
|
||||
extra := ""
|
||||
if pr.PeerAPIPort != 0 {
|
||||
|
||||
78
cmd/tailscale/cli/risks.go
Normal file
78
cmd/tailscale/cli/risks.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
riskTypes []string
|
||||
acceptedRisks string
|
||||
riskLoseSSH = registerRiskType("lose-ssh")
|
||||
)
|
||||
|
||||
func registerRiskType(riskType string) string {
|
||||
riskTypes = append(riskTypes, riskType)
|
||||
return riskType
|
||||
}
|
||||
|
||||
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
|
||||
// in presentRiskToUser.
|
||||
func registerAcceptRiskFlag(f *flag.FlagSet) {
|
||||
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
|
||||
}
|
||||
|
||||
// riskAccepted reports whether riskType is in acceptedRisks.
|
||||
func riskAccepted(riskType string) bool {
|
||||
for _, r := range strings.Split(acceptedRisks, ",") {
|
||||
if r == riskType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var errAborted = errors.New("aborted, no changes made")
|
||||
|
||||
// riskAbortTimeSeconds is the number of seconds to wait after displaying the
|
||||
// risk message before continuing with the operation.
|
||||
// It is used by the presentRiskToUser function below.
|
||||
const riskAbortTimeSeconds = 5
|
||||
|
||||
// presentRiskToUser displays the risk message and waits for the user to
|
||||
// cancel. It returns errorAborted if the user aborts.
|
||||
func presentRiskToUser(riskType, riskMessage string) error {
|
||||
if riskAccepted(riskType) {
|
||||
return nil
|
||||
}
|
||||
fmt.Println(riskMessage)
|
||||
fmt.Printf("To skip this warning, use --accept-risk=%s\n", riskType)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT)
|
||||
var msgLen int
|
||||
for left := riskAbortTimeSeconds; left > 0; left-- {
|
||||
msg := fmt.Sprintf("\rContinuing in %d seconds...", left)
|
||||
msgLen = len(msg)
|
||||
fmt.Print(msg)
|
||||
select {
|
||||
case <-interrupt:
|
||||
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen+1))
|
||||
return errAborted
|
||||
case <-time.After(time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen))
|
||||
return errAborted
|
||||
}
|
||||
@@ -16,12 +16,13 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var sshCmd = &ffcli.Command{
|
||||
@@ -32,6 +33,9 @@ var sshCmd = &ffcli.Command{
|
||||
}
|
||||
|
||||
func runSSH(ctx context.Context, args []string) error {
|
||||
if runtime.GOOS == "darwin" && version.IsSandboxedMacOS() && !envknob.UseWIPCode() {
|
||||
return errors.New("The 'tailscale ssh' subcommand is not available on sandboxed macOS builds.\nUse the regular 'ssh' client instead.")
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return errors.New("usage: ssh [user@]<host>")
|
||||
}
|
||||
@@ -116,24 +120,7 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
log.Printf("Running: %q, %q ...", ssh, argv)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Don't use syscall.Exec on Windows.
|
||||
cmd := exec.Command(ssh, argv[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
var ee *exec.ExitError
|
||||
err := cmd.Run()
|
||||
if errors.As(err, &ee) {
|
||||
os.Exit(ee.ExitCode())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("unreachable")
|
||||
return execSSH(ssh, argv)
|
||||
}
|
||||
|
||||
func writeKnownHosts(st *ipnstate.Status) (knownHostsFile string, err error) {
|
||||
@@ -197,3 +184,28 @@ func nodeDNSNameFromArg(st *ipnstate.Status, arg string) (dnsName string, ok boo
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// getSSHClientEnvVar returns the "SSH_CLIENT" environment variable
|
||||
// for the current process group, if any.
|
||||
var getSSHClientEnvVar = func() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// isSSHOverTailscale checks if the invocation is in a SSH session over Tailscale.
|
||||
// It is used to detect if the user is about to take an action that might result in them
|
||||
// disconnecting from the machine (e.g. disabling SSH)
|
||||
func isSSHOverTailscale() bool {
|
||||
sshClient := getSSHClientEnvVar()
|
||||
if sshClient == "" {
|
||||
return false
|
||||
}
|
||||
ipStr, _, ok := strings.Cut(sshClient, " ")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return tsaddr.IsTailscaleIP(ip)
|
||||
}
|
||||
|
||||
21
cmd/tailscale/cli/ssh_exec.go
Normal file
21
cmd/tailscale/cli/ssh_exec.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 !js && !windows
|
||||
// +build !js,!windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("unreachable")
|
||||
}
|
||||
13
cmd/tailscale/cli/ssh_exec_js.go
Normal file
13
cmd/tailscale/cli/ssh_exec_js.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
return errors.New("Not implemented")
|
||||
}
|
||||
25
cmd/tailscale/cli/ssh_exec_windows.go
Normal file
25
cmd/tailscale/cli/ssh_exec_windows.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
// Don't use syscall.Exec on Windows, it's not fully implemented.
|
||||
cmd := exec.Command(ssh, argv[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
var ee *exec.ExitError
|
||||
err := cmd.Run()
|
||||
if errors.As(err, &ee) {
|
||||
os.Exit(ee.ExitCode())
|
||||
}
|
||||
return err
|
||||
}
|
||||
51
cmd/tailscale/cli/ssh_unix.go
Normal file
51
cmd/tailscale/cli/ssh_unix.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 !js && !windows
|
||||
// +build !js,!windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
getSSHClientEnvVar = func() string {
|
||||
if os.Getenv("SUDO_USER") == "" {
|
||||
// No sudo, just check the env.
|
||||
return os.Getenv("SSH_CLIENT")
|
||||
}
|
||||
if runtime.GOOS != "linux" {
|
||||
// TODO(maisem): implement this for other platforms. It's not clear
|
||||
// if there is a way to get the environment for a given process on
|
||||
// darwin and bsd.
|
||||
return ""
|
||||
}
|
||||
// SID is the session ID of the user's login session.
|
||||
// It is also the process ID of the original shell that the user logged in with.
|
||||
// We only need to check the environment of that process.
|
||||
sid, err := unix.Getsid(os.Getpid())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(sid), "environ"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
prefix := []byte("SSH_CLIENT=")
|
||||
for _, env := range bytes.Split(b, []byte{0}) {
|
||||
if bytes.HasPrefix(env, prefix) {
|
||||
return string(env[len(prefix):])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -128,12 +128,8 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
outln(description)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// print health check information prior to checking LocalBackend state as
|
||||
// it may provide an explanation to the user if we choose to exit early
|
||||
if len(st.Health) > 0 {
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
@@ -142,6 +138,12 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
outln()
|
||||
}
|
||||
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
outln(description)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
@@ -114,6 +115,8 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
case "windows":
|
||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
|
||||
registerAcceptRiskFlag(upf)
|
||||
return upf
|
||||
}
|
||||
|
||||
@@ -146,6 +149,7 @@ type upArgsT struct {
|
||||
hostname string
|
||||
opUser string
|
||||
json bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
@@ -400,7 +404,7 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return simpleUp, justEditMP, nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
@@ -465,6 +469,24 @@ func runUp(ctx context.Context, args []string) error {
|
||||
backendState: st.BackendState,
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
}
|
||||
|
||||
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
|
||||
if upArgs.runSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
|
||||
} else {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr == nil {
|
||||
checkSSHUpWarnings(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
@@ -632,6 +654,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// need to prioritize reads from 'running' if it's
|
||||
// readable; its send does happen before the pump mechanism
|
||||
// shuts down. (Issue 2333)
|
||||
var timeoutCh <-chan time.Time
|
||||
if upArgs.timeout > 0 {
|
||||
timeoutTimer := time.NewTimer(upArgs.timeout)
|
||||
defer timeoutTimer.Stop()
|
||||
timeoutCh = timeoutTimer.C
|
||||
}
|
||||
select {
|
||||
case <-running:
|
||||
return nil
|
||||
@@ -649,6 +677,30 @@ func runUp(ctx context.Context, args []string) error {
|
||||
default:
|
||||
}
|
||||
return err
|
||||
case <-timeoutCh:
|
||||
return errors.New(`timeout waiting for Tailscale service to enter a Running state; check health with "tailscale status"`)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSSHUpWarnings(ctx context.Context) {
|
||||
if !upArgs.runSSH {
|
||||
return
|
||||
}
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
// Ignore. Don't spam more.
|
||||
return
|
||||
}
|
||||
if len(st.Health) == 0 {
|
||||
return
|
||||
}
|
||||
if len(st.Health) == 1 && strings.Contains(st.Health[0], "SSH") {
|
||||
printf("%s\n", st.Health[0])
|
||||
return
|
||||
}
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
printf(" - %s\n", m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,7 +757,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "auth-key", "force-reauth", "reset", "qr", "json":
|
||||
case "auth-key", "force-reauth", "reset", "qr", "json", "timeout", "accept-risk":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -8,7 +8,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
@@ -31,39 +31,42 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/wsconn from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -88,18 +91,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
W tailscale.com/util/winutil from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/hkdf from crypto/tls+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
|
||||
@@ -76,9 +76,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
@@ -113,7 +114,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
|
||||
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
|
||||
@@ -140,9 +141,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
@@ -155,44 +156,43 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/internal/network+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/raw+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/udp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
inet.af/netaddr from inet.af/wf+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlclient+
|
||||
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
|
||||
@@ -203,41 +203,42 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
💣 tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
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+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/net/dns+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
@@ -248,8 +249,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/key from tailscale.com/control/controlbase+
|
||||
tailscale.com/types/logger from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
@@ -258,7 +259,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/clientmetric from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
@@ -266,24 +267,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/netconv from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
@@ -321,7 +321,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
@@ -355,10 +355,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
embed from crypto/elliptic+
|
||||
embed from tailscale.com+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -366,19 +366,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
flag from tailscale.com/control/controlclient+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
html from tailscale.com/ipn/ipnlocal+
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
log from expvar+
|
||||
LD log/syslog from tailscale.com/ssh/tailssh
|
||||
math from compress/flate+
|
||||
@@ -395,19 +395,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
net/http/internal from net/http+
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled+
|
||||
net/netip from golang.zx2c4.com/wireguard/conn+
|
||||
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
|
||||
path from github.com/godbus/dbus/v5+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
runtime/debug from golang.org/x/sync/singleflight+
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
runtime/trace from net/http/pprof
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
|
||||
@@ -137,7 +137,7 @@ func main() {
|
||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state")
|
||||
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
||||
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
@@ -158,13 +158,12 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if beWindowsSubprocess() {
|
||||
return
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
if flag.NArg() > 0 {
|
||||
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
||||
// Windows subprocess is spawned with /subprocess, so we need to avoid this check there.
|
||||
if runtime.GOOS != "windows" || flag.Arg(0) != "/subproc" {
|
||||
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
||||
}
|
||||
}
|
||||
|
||||
if printVersion {
|
||||
@@ -187,6 +186,16 @@ func main() {
|
||||
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// Only apply a default statepath when neither have been provided, so that a
|
||||
// user may specify only --statedir if they wish.
|
||||
if args.statepath == "" && args.statedir == "" {
|
||||
args.statepath = paths.DefaultTailscaledStateFile()
|
||||
}
|
||||
|
||||
if beWindowsSubprocess() {
|
||||
return
|
||||
}
|
||||
|
||||
err := run()
|
||||
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
|
||||
@@ -260,19 +260,19 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("monitor: %w", err)
|
||||
}
|
||||
dialer := new(tsdial.Dialer)
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, error) {
|
||||
getEngineRaw := func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TUN: %w", err)
|
||||
return nil, nil, fmt.Errorf("TUN: %w", err)
|
||||
}
|
||||
r, err := router.New(logf, dev, nil)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("router: %w", err)
|
||||
return nil, nil, fmt.Errorf("router: %w", err)
|
||||
}
|
||||
if wrapNetstack {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
@@ -281,7 +281,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("DNS: %w", err)
|
||||
return nil, nil, fmt.Errorf("DNS: %w", err)
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
@@ -294,23 +294,24 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
return nil, nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
ns, err := newNetstack(logf, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
return nil, nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = false
|
||||
ns.ProcessSubnets = wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start netstack: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
return wgengine.NewWatchdog(eng), ns, nil
|
||||
}
|
||||
|
||||
type engineOrError struct {
|
||||
Engine wgengine.Engine
|
||||
Err error
|
||||
Engine wgengine.Engine
|
||||
Netstack *netstack.Impl
|
||||
Err error
|
||||
}
|
||||
engErrc := make(chan engineOrError)
|
||||
t0 := time.Now()
|
||||
@@ -319,7 +320,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
for try := 1; ; try++ {
|
||||
logf("tailscaled: getting engine... (try %v)", try)
|
||||
t1 := time.Now()
|
||||
eng, err := getEngineRaw()
|
||||
eng, ns, err := getEngineRaw()
|
||||
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
|
||||
if err != nil {
|
||||
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
|
||||
@@ -332,7 +333,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
engErrc <- engineOrError{eng, err}
|
||||
engErrc <- engineOrError{eng, ns, err}
|
||||
if err == nil {
|
||||
timer.Stop()
|
||||
return
|
||||
@@ -344,14 +345,14 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
// getEngine is called by ipnserver to get the engine. It's
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
getEngine := func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
return nil, nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
}
|
||||
for {
|
||||
res := <-engErrc
|
||||
if res.Engine != nil {
|
||||
return res.Engine, nil
|
||||
return res.Engine, res.Netstack, nil
|
||||
}
|
||||
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
|
||||
// Ignore errors during early boot. Windows 10 auto logs in the GUI
|
||||
@@ -362,12 +363,12 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
}
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
return nil, nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
}
|
||||
}
|
||||
store, err := store.New(logf, statePathOrDefault())
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("store: %w", err)
|
||||
}
|
||||
|
||||
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
||||
|
||||
@@ -2,177 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// The tsshd binary is an SSH server that accepts connections
|
||||
// The tsshd binary was an experimental SSH server that accepts connections
|
||||
// from anybody on the same Tailscale network.
|
||||
//
|
||||
// It does not use passwords or SSH public key.
|
||||
// Its functionality moved into tailscaled.
|
||||
//
|
||||
// Any user name is accepted; users are logged in as whoever is
|
||||
// running this daemon.
|
||||
//
|
||||
// Warning: use at your own risk. This code has had very few eyeballs
|
||||
// on it.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/creack/pty"
|
||||
gossh "github.com/tailscale/golang-x-crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
port = flag.Int("port", 2200, "port to listen on")
|
||||
hostKey = flag.String("hostkey", "", "SSH host key")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *hostKey == "" {
|
||||
log.Fatalf("missing required --hostkey")
|
||||
}
|
||||
hostKey, err := ioutil.ReadFile(*hostKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse SSH host key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
warned := false
|
||||
for {
|
||||
addrs, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
log.Fatalf("listing interfaces: %v", err)
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
if !warned {
|
||||
log.Printf("no tailscale interface found; polling until one is available")
|
||||
warned = true
|
||||
}
|
||||
// TODO: use netlink or other OS-specific mechanism to efficiently
|
||||
// wait for change in interfaces. Polling every N seconds is good enough
|
||||
// for now.
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
warned = false
|
||||
var addr netaddr.IP
|
||||
for _, a := range addrs {
|
||||
if a.Is4() {
|
||||
addr = a
|
||||
break
|
||||
}
|
||||
}
|
||||
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port))
|
||||
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen)
|
||||
s := &ssh.Server{
|
||||
Addr: listen,
|
||||
Handler: handleSSH,
|
||||
}
|
||||
s.AddHostKey(signer)
|
||||
|
||||
err = s.ListenAndServe()
|
||||
log.Fatalf("tailscale sshd failed: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleSSH(s ssh.Session) {
|
||||
user := s.User()
|
||||
addr := s.RemoteAddr()
|
||||
ta, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
log.Printf("tsshd: rejecting non-TCP addr %T %v", addr, addr)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
|
||||
if !ok {
|
||||
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(tanetaddr) {
|
||||
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("new session for %q from %v", user, ta)
|
||||
defer log.Printf("closing session for %q from %v", user, ta)
|
||||
ptyReq, winCh, isPty := s.Pty()
|
||||
if !isPty {
|
||||
fmt.Fprintf(s, "TODO scp etc")
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
userWantsShell := len(s.Command()) == 0
|
||||
|
||||
if userWantsShell {
|
||||
shell, err := shellOfUser(s.User())
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "failed to find shell: %v\n", err)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(shell)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||
f, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
log.Printf("running shell: %v", err)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
go func() {
|
||||
for win := range winCh {
|
||||
setWinsize(f, win.Width, win.Height)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(f, s) // stdin
|
||||
}()
|
||||
io.Copy(s, f) // stdout
|
||||
cmd.Process.Kill()
|
||||
if err := cmd.Wait(); err != nil {
|
||||
s.Exit(1)
|
||||
} else {
|
||||
s.Exit(0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(s, "TODO: args\n")
|
||||
s.Exit(1)
|
||||
}
|
||||
|
||||
func shellOfUser(user string) (string, error) {
|
||||
// TODO
|
||||
return "/bin/bash", nil
|
||||
}
|
||||
|
||||
func setWinsize(f *os.File, w, h int) {
|
||||
syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
|
||||
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
|
||||
}
|
||||
// See https://github.com/tailscale/tailscale/issues/3802
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
func main() {
|
||||
panic("tsshd does not work on windows yet")
|
||||
}
|
||||
59
cmd/viewer/tests/tests.go
Normal file
59
cmd/viewer/tests/tests.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 tests serves a list of tests for tailscale.com/cmd/viewer.
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
|
||||
|
||||
type StructWithoutPtrs struct {
|
||||
Int int
|
||||
Pfx netaddr.IPPrefix
|
||||
}
|
||||
|
||||
type Map struct {
|
||||
Int map[string]int
|
||||
SliceInt map[string][]int
|
||||
StructWithPtr map[string]*StructWithPtrs
|
||||
StructWithoutPtr map[string]*StructWithoutPtrs
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int `json:"-"`
|
||||
|
||||
// Unsupported.
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int `json:"-"`
|
||||
StructWithPtrKey map[StructWithPtrs]int `json:"-"`
|
||||
}
|
||||
|
||||
type StructWithPtrs struct {
|
||||
Value *StructWithoutPtrs
|
||||
Int *int
|
||||
|
||||
NoCloneValue *StructWithoutPtrs `codegen:"noclone"`
|
||||
}
|
||||
|
||||
func (v *StructWithPtrs) String() string { return fmt.Sprintf("%v", v.Int) }
|
||||
|
||||
func (v *StructWithPtrs) Equal(v2 *StructWithPtrs) bool {
|
||||
return v.Value == v2.Value
|
||||
}
|
||||
|
||||
type StructWithSlices struct {
|
||||
Values []StructWithoutPtrs
|
||||
ValuePointers []*StructWithoutPtrs
|
||||
StructPointers []*StructWithPtrs
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
|
||||
Slice []string
|
||||
Prefixes []netaddr.IPPrefix
|
||||
Data []byte
|
||||
}
|
||||
183
cmd/viewer/tests/tests_clone.go
Normal file
183
cmd/viewer/tests/tests_clone.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of StructWithPtrs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *StructWithPtrs) Clone() *StructWithPtrs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(StructWithPtrs)
|
||||
*dst = *src
|
||||
if dst.Value != nil {
|
||||
dst.Value = new(StructWithoutPtrs)
|
||||
*dst.Value = *src.Value
|
||||
}
|
||||
if dst.Int != nil {
|
||||
dst.Int = new(int)
|
||||
*dst.Int = *src.Int
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithPtrsCloneNeedsRegeneration = StructWithPtrs(struct {
|
||||
Value *StructWithoutPtrs
|
||||
Int *int
|
||||
NoCloneValue *StructWithoutPtrs
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of StructWithoutPtrs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *StructWithoutPtrs) Clone() *StructWithoutPtrs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(StructWithoutPtrs)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithoutPtrsCloneNeedsRegeneration = StructWithoutPtrs(struct {
|
||||
Int int
|
||||
Pfx netaddr.IPPrefix
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Map.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Map) Clone() *Map {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Map)
|
||||
*dst = *src
|
||||
if dst.Int != nil {
|
||||
dst.Int = map[string]int{}
|
||||
for k, v := range src.Int {
|
||||
dst.Int[k] = v
|
||||
}
|
||||
}
|
||||
if dst.SliceInt != nil {
|
||||
dst.SliceInt = map[string][]int{}
|
||||
for k := range src.SliceInt {
|
||||
dst.SliceInt[k] = append([]int{}, src.SliceInt[k]...)
|
||||
}
|
||||
}
|
||||
if dst.StructWithPtr != nil {
|
||||
dst.StructWithPtr = map[string]*StructWithPtrs{}
|
||||
for k, v := range src.StructWithPtr {
|
||||
dst.StructWithPtr[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
if dst.StructWithoutPtr != nil {
|
||||
dst.StructWithoutPtr = map[string]*StructWithoutPtrs{}
|
||||
for k, v := range src.StructWithoutPtr {
|
||||
dst.StructWithoutPtr[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
if dst.SlicesWithPtrs != nil {
|
||||
dst.SlicesWithPtrs = map[string][]*StructWithPtrs{}
|
||||
for k := range src.SlicesWithPtrs {
|
||||
dst.SlicesWithPtrs[k] = append([]*StructWithPtrs{}, src.SlicesWithPtrs[k]...)
|
||||
}
|
||||
}
|
||||
if dst.SlicesWithoutPtrs != nil {
|
||||
dst.SlicesWithoutPtrs = map[string][]*StructWithoutPtrs{}
|
||||
for k := range src.SlicesWithoutPtrs {
|
||||
dst.SlicesWithoutPtrs[k] = append([]*StructWithoutPtrs{}, src.SlicesWithoutPtrs[k]...)
|
||||
}
|
||||
}
|
||||
if dst.StructWithoutPtrKey != nil {
|
||||
dst.StructWithoutPtrKey = map[StructWithoutPtrs]int{}
|
||||
for k, v := range src.StructWithoutPtrKey {
|
||||
dst.StructWithoutPtrKey[k] = v
|
||||
}
|
||||
}
|
||||
if dst.SliceIntPtr != nil {
|
||||
dst.SliceIntPtr = map[string][]*int{}
|
||||
for k := range src.SliceIntPtr {
|
||||
dst.SliceIntPtr[k] = append([]*int{}, src.SliceIntPtr[k]...)
|
||||
}
|
||||
}
|
||||
if dst.PointerKey != nil {
|
||||
dst.PointerKey = map[*string]int{}
|
||||
for k, v := range src.PointerKey {
|
||||
dst.PointerKey[k] = v
|
||||
}
|
||||
}
|
||||
if dst.StructWithPtrKey != nil {
|
||||
dst.StructWithPtrKey = map[StructWithPtrs]int{}
|
||||
for k, v := range src.StructWithPtrKey {
|
||||
dst.StructWithPtrKey[k] = v
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _MapCloneNeedsRegeneration = Map(struct {
|
||||
Int map[string]int
|
||||
SliceInt map[string][]int
|
||||
StructWithPtr map[string]*StructWithPtrs
|
||||
StructWithoutPtr map[string]*StructWithoutPtrs
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int
|
||||
StructWithPtrKey map[StructWithPtrs]int
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of StructWithSlices.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *StructWithSlices) Clone() *StructWithSlices {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(StructWithSlices)
|
||||
*dst = *src
|
||||
dst.Values = append(src.Values[:0:0], src.Values...)
|
||||
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
|
||||
for i := range dst.ValuePointers {
|
||||
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
|
||||
}
|
||||
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
|
||||
for i := range dst.StructPointers {
|
||||
dst.StructPointers[i] = src.StructPointers[i].Clone()
|
||||
}
|
||||
dst.Structs = make([]StructWithPtrs, len(src.Structs))
|
||||
for i := range dst.Structs {
|
||||
dst.Structs[i] = *src.Structs[i].Clone()
|
||||
}
|
||||
dst.Ints = make([]*int, len(src.Ints))
|
||||
for i := range dst.Ints {
|
||||
x := *src.Ints[i]
|
||||
dst.Ints[i] = &x
|
||||
}
|
||||
dst.Slice = append(src.Slice[:0:0], src.Slice...)
|
||||
dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...)
|
||||
dst.Data = append(src.Data[:0:0], src.Data...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct {
|
||||
Values []StructWithoutPtrs
|
||||
ValuePointers []*StructWithoutPtrs
|
||||
StructPointers []*StructWithPtrs
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
Slice []string
|
||||
Prefixes []netaddr.IPPrefix
|
||||
Data []byte
|
||||
}{})
|
||||
316
cmd/viewer/tests/tests_view.go
Normal file
316
cmd/viewer/tests/tests_view.go
Normal file
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
|
||||
|
||||
// View returns a readonly view of StructWithPtrs.
|
||||
func (p *StructWithPtrs) View() StructWithPtrsView {
|
||||
return StructWithPtrsView{ж: p}
|
||||
}
|
||||
|
||||
// StructWithPtrsView provides a read-only view over StructWithPtrs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type StructWithPtrsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructWithPtrs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v StructWithPtrsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v StructWithPtrsView) AsStruct() *StructWithPtrs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v StructWithPtrsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *StructWithPtrsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x StructWithPtrs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v StructWithPtrsView) Value() *StructWithoutPtrs {
|
||||
if v.ж.Value == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Value
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v StructWithPtrsView) Int() *int {
|
||||
if v.ж.Int == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Int
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v StructWithPtrsView) NoCloneValue() *StructWithoutPtrs { return v.ж.NoCloneValue }
|
||||
func (v StructWithPtrsView) String() string { return v.ж.String() }
|
||||
func (v StructWithPtrsView) Equal(v2 StructWithPtrsView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithPtrsViewNeedsRegeneration = StructWithPtrs(struct {
|
||||
Value *StructWithoutPtrs
|
||||
Int *int
|
||||
NoCloneValue *StructWithoutPtrs
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of StructWithoutPtrs.
|
||||
func (p *StructWithoutPtrs) View() StructWithoutPtrsView {
|
||||
return StructWithoutPtrsView{ж: p}
|
||||
}
|
||||
|
||||
// StructWithoutPtrsView provides a read-only view over StructWithoutPtrs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type StructWithoutPtrsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructWithoutPtrs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v StructWithoutPtrsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v StructWithoutPtrsView) AsStruct() *StructWithoutPtrs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v StructWithoutPtrsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *StructWithoutPtrsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x StructWithoutPtrs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v StructWithoutPtrsView) Int() int { return v.ж.Int }
|
||||
func (v StructWithoutPtrsView) Pfx() netaddr.IPPrefix { return v.ж.Pfx }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithoutPtrsViewNeedsRegeneration = StructWithoutPtrs(struct {
|
||||
Int int
|
||||
Pfx netaddr.IPPrefix
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Map.
|
||||
func (p *Map) View() MapView {
|
||||
return MapView{ж: p}
|
||||
}
|
||||
|
||||
// MapView provides a read-only view over Map.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type MapView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Map
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v MapView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v MapView) AsStruct() *Map {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v MapView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *MapView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Map
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v MapView) Int() views.Map[string, int] { return views.MapOf(v.ж.Int) }
|
||||
|
||||
func (v MapView) SliceInt() views.MapFn[string, []int, views.Slice[int]] {
|
||||
return views.MapFnOf(v.ж.SliceInt, func(t []int) views.Slice[int] {
|
||||
return views.SliceOf(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) StructWithPtr() views.MapFn[string, *StructWithPtrs, StructWithPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructWithPtr, func(t *StructWithPtrs) StructWithPtrsView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) StructWithoutPtr() views.MapFn[string, *StructWithoutPtrs, StructWithoutPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructWithoutPtr, func(t *StructWithoutPtrs) StructWithoutPtrsView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) SlicesWithPtrs() views.MapFn[string, []*StructWithPtrs, views.SliceView[*StructWithPtrs, StructWithPtrsView]] {
|
||||
return views.MapFnOf(v.ж.SlicesWithPtrs, func(t []*StructWithPtrs) views.SliceView[*StructWithPtrs, StructWithPtrsView] {
|
||||
return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](t)
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) SlicesWithoutPtrs() views.MapFn[string, []*StructWithoutPtrs, views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView]] {
|
||||
return views.MapFnOf(v.ж.SlicesWithoutPtrs, func(t []*StructWithoutPtrs) views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] {
|
||||
return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](t)
|
||||
})
|
||||
}
|
||||
|
||||
func (v MapView) StructWithoutPtrKey() views.Map[StructWithoutPtrs, int] {
|
||||
return views.MapOf(v.ж.StructWithoutPtrKey)
|
||||
}
|
||||
func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") }
|
||||
func (v MapView) PointerKey() map[*string]int { panic("unsupported") }
|
||||
func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _MapViewNeedsRegeneration = Map(struct {
|
||||
Int map[string]int
|
||||
SliceInt map[string][]int
|
||||
StructWithPtr map[string]*StructWithPtrs
|
||||
StructWithoutPtr map[string]*StructWithoutPtrs
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int
|
||||
StructWithPtrKey map[StructWithPtrs]int
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of StructWithSlices.
|
||||
func (p *StructWithSlices) View() StructWithSlicesView {
|
||||
return StructWithSlicesView{ж: p}
|
||||
}
|
||||
|
||||
// StructWithSlicesView provides a read-only view over StructWithSlices.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type StructWithSlicesView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructWithSlices
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v StructWithSlicesView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v StructWithSlicesView) AsStruct() *StructWithSlices {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v StructWithSlicesView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *StructWithSlicesView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x StructWithSlices
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v StructWithSlicesView) Values() views.Slice[StructWithoutPtrs] {
|
||||
return views.SliceOf(v.ж.Values)
|
||||
}
|
||||
func (v StructWithSlicesView) ValuePointers() views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] {
|
||||
return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](v.ж.ValuePointers)
|
||||
}
|
||||
func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs, StructWithPtrsView] {
|
||||
return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](v.ж.StructPointers)
|
||||
}
|
||||
func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) }
|
||||
func (v StructWithSlicesView) Prefixes() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.Prefixes)
|
||||
}
|
||||
func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
|
||||
Values []StructWithoutPtrs
|
||||
ValuePointers []*StructWithoutPtrs
|
||||
StructPointers []*StructWithPtrs
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
Slice []string
|
||||
Prefixes []netaddr.IPPrefix
|
||||
Data []byte
|
||||
}{})
|
||||
378
cmd/viewer/viewer.go
Normal file
378
cmd/viewer/viewer.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// 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.
|
||||
|
||||
// Viewer is a tool to automate the creation of "view" wrapper types that
|
||||
// provide read-only accessor methods to underlying fields.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/codegen"
|
||||
)
|
||||
|
||||
const viewTemplateStr = `{{define "common"}}
|
||||
// View returns a readonly view of {{.StructName}}.
|
||||
func (p *{{.StructName}}) View() {{.ViewName}} {
|
||||
return {{.ViewName}}{ж: p}
|
||||
}
|
||||
|
||||
// {{.ViewName}} provides a read-only view over {{.StructName}}.
|
||||
//
|
||||
// Its methods should only be called if ` + "`Valid()`" + ` returns true.
|
||||
type {{.ViewName}} struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *{{.StructName}}
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v {{.ViewName}}) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v {{.ViewName}}) AsStruct() *{{.StructName}}{
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v {{.ViewName}}) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x {{.StructName}}
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж=&x
|
||||
return nil
|
||||
}
|
||||
|
||||
{{end}}
|
||||
{{define "valueField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} }
|
||||
{{end}}
|
||||
{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "ipPrefixSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "viewField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}}View { return v.ж.{{.FieldName}}.View() }
|
||||
{{end}}
|
||||
{{define "valuePointerField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {
|
||||
if v.ж.{{.FieldName}} == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.{{.FieldName}}
|
||||
return &x
|
||||
}
|
||||
|
||||
{{end}}
|
||||
{{define "mapField"}}
|
||||
func(v {{.ViewName}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})}
|
||||
{{end}}
|
||||
{{define "mapFnField"}}
|
||||
func(v {{.ViewName}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} {
|
||||
return {{.MapFn}}
|
||||
})}
|
||||
{{end}}
|
||||
{{define "unsupportedField"}}func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
|
||||
{{end}}
|
||||
{{define "stringFunc"}}func(v {{.ViewName}}) String() string { return v.ж.String() }
|
||||
{{end}}
|
||||
{{define "equalFunc"}}func(v {{.ViewName}}) Equal(v2 {{.ViewName}}) bool { return v.ж.Equal(v2.ж) }
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var viewTemplate *template.Template
|
||||
|
||||
func init() {
|
||||
viewTemplate = template.Must(template.New("view").Parse(viewTemplateStr))
|
||||
}
|
||||
|
||||
func requiresCloning(t types.Type) (shallow, deep bool, base types.Type) {
|
||||
switch v := t.(type) {
|
||||
case *types.Pointer:
|
||||
_, deep, base = requiresCloning(v.Elem())
|
||||
return true, deep, base
|
||||
case *types.Slice:
|
||||
_, deep, base = requiresCloning(v.Elem())
|
||||
return true, deep, base
|
||||
}
|
||||
p := codegen.ContainsPointers(t)
|
||||
return p, p, t
|
||||
}
|
||||
|
||||
func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thisPkg *types.Package) {
|
||||
t, ok := typ.Underlying().(*types.Struct)
|
||||
if !ok || codegen.IsViewType(t) {
|
||||
return
|
||||
}
|
||||
it.Import("encoding/json")
|
||||
it.Import("errors")
|
||||
|
||||
args := struct {
|
||||
StructName string
|
||||
ViewName string
|
||||
FieldName string
|
||||
FieldType string
|
||||
FieldViewName string
|
||||
|
||||
MapKeyType string
|
||||
MapValueType string
|
||||
MapValueView string
|
||||
MapFn string
|
||||
}{
|
||||
StructName: typ.Obj().Name(),
|
||||
ViewName: typ.Obj().Name() + "View",
|
||||
}
|
||||
|
||||
writeTemplate := func(name string) {
|
||||
if err := viewTemplate.ExecuteTemplate(buf, name, args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeTemplate("common")
|
||||
for i := 0; i < t.NumFields(); i++ {
|
||||
f := t.Field(i)
|
||||
fname := f.Name()
|
||||
if !f.Exported() {
|
||||
continue
|
||||
}
|
||||
args.FieldName = fname
|
||||
fieldType := f.Type()
|
||||
if codegen.IsInvalid(fieldType) {
|
||||
continue
|
||||
}
|
||||
if !codegen.ContainsPointers(fieldType) || codegen.IsViewType(fieldType) || codegen.HasNoClone(t.Tag(i)) {
|
||||
args.FieldType = it.QualifiedName(fieldType)
|
||||
writeTemplate("valueField")
|
||||
continue
|
||||
}
|
||||
switch underlying := fieldType.Underlying().(type) {
|
||||
case *types.Slice:
|
||||
slice := underlying
|
||||
elem := slice.Elem()
|
||||
args.FieldType = it.QualifiedName(elem)
|
||||
switch elem.String() {
|
||||
case "byte":
|
||||
it.Import("go4.org/mem")
|
||||
writeTemplate("byteSliceField")
|
||||
case "inet.af/netaddr.IPPrefix":
|
||||
it.Import("tailscale.com/types/views")
|
||||
writeTemplate("ipPrefixSliceField")
|
||||
default:
|
||||
it.Import("tailscale.com/types/views")
|
||||
shallow, deep, base := requiresCloning(elem)
|
||||
if deep {
|
||||
if _, isPtr := elem.(*types.Pointer); isPtr {
|
||||
args.FieldViewName = it.QualifiedName(base) + "View"
|
||||
writeTemplate("viewSliceField")
|
||||
} else {
|
||||
writeTemplate("unsupportedField")
|
||||
}
|
||||
continue
|
||||
} else if shallow {
|
||||
if _, isBasic := base.(*types.Basic); isBasic {
|
||||
writeTemplate("unsupportedField")
|
||||
} else {
|
||||
args.FieldViewName = it.QualifiedName(base) + "View"
|
||||
writeTemplate("viewSliceField")
|
||||
}
|
||||
continue
|
||||
}
|
||||
writeTemplate("sliceField")
|
||||
}
|
||||
continue
|
||||
case *types.Struct, *types.Named:
|
||||
strucT := underlying
|
||||
args.FieldType = it.QualifiedName(fieldType)
|
||||
if codegen.ContainsPointers(strucT) {
|
||||
writeTemplate("viewField")
|
||||
continue
|
||||
}
|
||||
writeTemplate("valueField")
|
||||
continue
|
||||
case *types.Map:
|
||||
m := underlying
|
||||
args.FieldType = it.QualifiedName(fieldType)
|
||||
shallow, deep, key := requiresCloning(m.Key())
|
||||
if shallow || deep {
|
||||
writeTemplate("unsupportedField")
|
||||
continue
|
||||
}
|
||||
args.MapKeyType = it.QualifiedName(key)
|
||||
mElem := m.Elem()
|
||||
var template string
|
||||
switch u := mElem.(type) {
|
||||
case *types.Basic:
|
||||
template = "mapField"
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
case *types.Slice:
|
||||
slice := u
|
||||
sElem := slice.Elem()
|
||||
switch x := sElem.(type) {
|
||||
case *types.Basic:
|
||||
args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
|
||||
args.MapValueType = "[]" + sElem.String()
|
||||
args.MapFn = "views.SliceOf(t)"
|
||||
template = "mapFnField"
|
||||
case *types.Pointer:
|
||||
ptr := x
|
||||
pElem := ptr.Elem()
|
||||
switch pElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
ptrType := it.QualifiedName(ptr)
|
||||
viewType := it.QualifiedName(pElem) + "View"
|
||||
args.MapFn = fmt.Sprintf("views.SliceOfViews[%v,%v](t)", ptrType, viewType)
|
||||
args.MapValueView = fmt.Sprintf("views.SliceView[%v,%v]", ptrType, viewType)
|
||||
args.MapValueType = "[]" + ptrType
|
||||
template = "mapFnField"
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
}
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
}
|
||||
case *types.Pointer:
|
||||
ptr := u
|
||||
pElem := ptr.Elem()
|
||||
switch pElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
args.MapValueType = it.QualifiedName(ptr)
|
||||
args.MapValueView = it.QualifiedName(pElem) + "View"
|
||||
args.MapFn = "t.View()"
|
||||
template = "mapFnField"
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
}
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
}
|
||||
writeTemplate(template)
|
||||
continue
|
||||
case *types.Pointer:
|
||||
ptr := underlying
|
||||
_, deep, base := requiresCloning(ptr)
|
||||
if deep {
|
||||
args.FieldType = it.QualifiedName(base)
|
||||
writeTemplate("viewField")
|
||||
} else {
|
||||
args.FieldType = it.QualifiedName(ptr)
|
||||
writeTemplate("valuePointerField")
|
||||
}
|
||||
continue
|
||||
}
|
||||
writeTemplate("unsupportedField")
|
||||
}
|
||||
for i := 0; i < typ.NumMethods(); i++ {
|
||||
f := typ.Method(i)
|
||||
if !f.Exported() {
|
||||
continue
|
||||
}
|
||||
sig, ok := f.Type().(*types.Signature)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch f.Name() {
|
||||
case "Clone", "View":
|
||||
continue // "AsStruct"
|
||||
case "String":
|
||||
writeTemplate("stringFunc")
|
||||
continue
|
||||
case "Equal":
|
||||
if sig.Results().Len() == 1 && sig.Results().At(0).Type().String() == "bool" {
|
||||
writeTemplate("equalFunc")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
buf.Write(codegen.AssertStructUnchanged(t, args.StructName, "View", it))
|
||||
}
|
||||
|
||||
var (
|
||||
flagTypes = flag.String("type", "", "comma-separated list of types; required")
|
||||
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
|
||||
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("viewer: ")
|
||||
flag.Parse()
|
||||
if len(*flagTypes) == 0 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
typeNames := strings.Split(*flagTypes, ",")
|
||||
|
||||
var flagArgs []string
|
||||
flagArgs = append(flagArgs, fmt.Sprintf("-clonefunc=%v", *flagCloneFunc))
|
||||
if *flagTypes != "" {
|
||||
flagArgs = append(flagArgs, "-type="+*flagTypes)
|
||||
}
|
||||
if *flagBuildTags != "" {
|
||||
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
|
||||
}
|
||||
pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
it := codegen.NewImportTracker(pkg.Types)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `//go:generate go run tailscale.com/cmd/cloner %s`, strings.Join(flagArgs, " "))
|
||||
fmt.Fprintln(buf)
|
||||
runCloner := false
|
||||
for _, typeName := range typeNames {
|
||||
typ, ok := namedTypes[typeName]
|
||||
if !ok {
|
||||
log.Fatalf("could not find type %s", typeName)
|
||||
}
|
||||
var hasClone bool
|
||||
for i, n := 0, typ.NumMethods(); i < n; i++ {
|
||||
if typ.Method(i).Name() == "Clone" {
|
||||
hasClone = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasClone {
|
||||
runCloner = true
|
||||
}
|
||||
genView(buf, it, typ, pkg.Types)
|
||||
}
|
||||
out := pkg.Name + "_view.go"
|
||||
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if runCloner {
|
||||
// When a new pacakge is added or when existing generated files have
|
||||
// been deleted, we might run into a case where tailscale.com/cmd/cloner
|
||||
// has not run yet. We detect this by verifying that all the structs we
|
||||
// interacted with have had Clone method already generated. If they
|
||||
// haven't we ask the caller to rerun generation again so that those get
|
||||
// generated.
|
||||
log.Printf("%v requires regeneration. Please run go generate again", pkg.Name+"_clone.go")
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,7 @@ func (c *Auto) authRoutine() {
|
||||
}
|
||||
|
||||
if goal == nil {
|
||||
health.SetAuthRoutineInError(nil)
|
||||
// Wait for user to Login or Logout.
|
||||
<-ctx.Done()
|
||||
c.logf("[v1] authRoutine: context done.")
|
||||
@@ -296,6 +297,7 @@ func (c *Auto) authRoutine() {
|
||||
}
|
||||
|
||||
if !goal.wantLoggedIn {
|
||||
health.SetAuthRoutineInError(nil)
|
||||
err := c.direct.TryLogout(ctx)
|
||||
goal.sendLogoutError(err)
|
||||
if err != nil {
|
||||
@@ -334,6 +336,7 @@ func (c *Auto) authRoutine() {
|
||||
f = "TryLogin"
|
||||
}
|
||||
if err != nil {
|
||||
health.SetAuthRoutineInError(err)
|
||||
report(err, f)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
@@ -358,6 +361,7 @@ func (c *Auto) authRoutine() {
|
||||
}
|
||||
|
||||
// success
|
||||
health.SetAuthRoutineInError(nil)
|
||||
c.mu.Lock()
|
||||
c.loggedIn = true
|
||||
c.loginGoal = nil
|
||||
|
||||
@@ -124,11 +124,10 @@ type Options struct {
|
||||
Pinger Pinger
|
||||
}
|
||||
|
||||
// Pinger is a subset of the wgengine.Engine interface, containing just the Ping method.
|
||||
// Pinger is the LocalBackend.Ping method.
|
||||
type Pinger interface {
|
||||
// Ping is a request to start a ping with the peer handling the given IP and
|
||||
// then call cb with its ping latency & method.
|
||||
Ping(ip netaddr.IP, pingType tailcfg.PingType, cb func(*ipnstate.PingResult))
|
||||
// Ping is a request to do a ping with the peer handling the given IP.
|
||||
Ping(ctx context.Context, ip netaddr.IP, pingType tailcfg.PingType) (*ipnstate.PingResult, error)
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -1208,10 +1207,8 @@ func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinge
|
||||
}
|
||||
for _, t := range strings.Split(pr.Types, ",") {
|
||||
switch pt := tailcfg.PingType(t); pt {
|
||||
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP:
|
||||
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP, tailcfg.PingPeerAPI:
|
||||
go doPingerPing(logf, c, pr, pinger, pt)
|
||||
// TODO(tailscale/corp#754)
|
||||
// case "peerapi":
|
||||
default:
|
||||
logf("unsupported ping request type: %q", t)
|
||||
}
|
||||
@@ -1417,10 +1414,17 @@ func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pin
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
pinger.Ping(pr.IP, pingType, func(res *ipnstate.PingResult) {
|
||||
// Currently does not check for error since we just return if it fails.
|
||||
postPingResult(start, logf, c, pr, res.ToPingResponse(pingType))
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := pinger.Ping(ctx, pr.IP, pingType)
|
||||
if err != nil {
|
||||
d := time.Since(start).Round(time.Millisecond)
|
||||
logf("doPingerPing: ping error of type %q to %v after %v: %v", pingType, pr.IP, d, err)
|
||||
return
|
||||
}
|
||||
postPingResult(start, logf, c, pr, res.ToPingResponse(pingType))
|
||||
}
|
||||
|
||||
func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *tailcfg.PingResponse) error {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// Package controlhttp implements the Tailscale 2021 control protocol
|
||||
// base transport over HTTP.
|
||||
//
|
||||
@@ -40,21 +43,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
const (
|
||||
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
upgradeHeaderValue = "tailscale-control-protocol"
|
||||
|
||||
// handshakeHeaderName is the HTTP request header that can
|
||||
// optionally contain base64-encoded initial handshake
|
||||
// payload, to save an RTT.
|
||||
handshakeHeaderName = "X-Tailscale-Handshake"
|
||||
|
||||
// serverUpgradePath is where the server-side HTTP handler to
|
||||
// to do the protocol switch is located.
|
||||
serverUpgradePath = "/ts2021"
|
||||
)
|
||||
|
||||
// Dial connects to the HTTP server at addr, requests to switch to the
|
||||
// Tailscale control protocol, and returns an established control
|
||||
// protocol connection.
|
||||
|
||||
56
control/controlhttp/client_js.go
Normal file
56
control/controlhttp/client_js.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 controlhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Variant of Dial that tunnels the request over WebScokets, since we cannot do
|
||||
// bi-directional communication over an HTTP connection when in JS.
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host, addr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsURL := &url.URL{
|
||||
Scheme: "ws",
|
||||
Host: net.JoinHostPort(host, addr),
|
||||
Path: serverUpgradePath,
|
||||
// Can't set HTTP headers on the websocket request, so we have to to send
|
||||
// the handshake via an HTTP header.
|
||||
RawQuery: url.Values{
|
||||
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
|
||||
}.Encode(),
|
||||
}
|
||||
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
|
||||
Subprotocols: []string{upgradeHeaderValue},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn := wsconn.New(wsConn)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return cbConn, nil
|
||||
|
||||
}
|
||||
20
control/controlhttp/constants.go
Normal file
20
control/controlhttp/constants.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 controlhttp
|
||||
|
||||
const (
|
||||
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
upgradeHeaderValue = "tailscale-control-protocol"
|
||||
|
||||
// handshakeHeaderName is the HTTP request header that can
|
||||
// optionally contain base64-encoded initial handshake
|
||||
// payload, to save an RTT.
|
||||
handshakeHeaderName = "X-Tailscale-Handshake"
|
||||
|
||||
// serverUpgradePath is where the server-side HTTP handler to
|
||||
// to do the protocol switch is located.
|
||||
serverUpgradePath = "/ts2021"
|
||||
)
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -27,6 +29,9 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
|
||||
http.Error(w, "missing next protocol", http.StatusBadRequest)
|
||||
return nil, errors.New("no next protocol in HTTP request")
|
||||
}
|
||||
if next == "websocket" {
|
||||
return acceptWebsocket(ctx, w, r, private)
|
||||
}
|
||||
if next != upgradeHeaderValue {
|
||||
http.Error(w, "unknown next protocol", http.StatusBadRequest)
|
||||
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
|
||||
@@ -71,3 +76,42 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
|
||||
// speak HTTP) to a Tailscale control protocol base transport connection.
|
||||
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{upgradeHeaderValue},
|
||||
OriginPatterns: []string{"*"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
|
||||
}
|
||||
if c.Subprotocol() != upgradeHeaderValue {
|
||||
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
|
||||
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
|
||||
return nil, fmt.Errorf("parse query parameters: %v", err)
|
||||
}
|
||||
initB64 := r.Form.Get(handshakeHeaderName)
|
||||
if initB64 == "" {
|
||||
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
|
||||
return nil, errors.New("no tailscale handshake parameter in HTTP request")
|
||||
}
|
||||
init, err := base64.StdEncoding.DecodeString(initB64)
|
||||
if err != nil {
|
||||
c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
|
||||
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
|
||||
}
|
||||
|
||||
conn := wsconn.New(c)
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("noise handshake failed: %w", err)
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp/wsconn"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,7 +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.
|
||||
|
||||
FROM ghcr.io/tailscale/tailscale:latest
|
||||
COPY run.sh /run.sh
|
||||
CMD "/run.sh"
|
||||
@@ -1,38 +1,28 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
|
||||
ifndef IMAGE_TAG
|
||||
$(error "IMAGE_TAG is not set")
|
||||
endif
|
||||
|
||||
ROUTES ?= ""
|
||||
SA_NAME ?= tailscale
|
||||
KUBE_SECRET ?= tailscale
|
||||
|
||||
build:
|
||||
@docker build . -t $(IMAGE_TAG)
|
||||
|
||||
push: build
|
||||
@docker push $(IMAGE_TAG)
|
||||
|
||||
rbac:
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
|
||||
|
||||
sidecar:
|
||||
@kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | kubectl create -f-
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
|
||||
|
||||
userspace-sidecar:
|
||||
@kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | kubectl create -f-
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
|
||||
|
||||
proxy:
|
||||
@kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{DEST_IP}};$(DEST_IP);g" | kubectl create -f-
|
||||
kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
|
||||
sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" | kubectl create -f-
|
||||
|
||||
subnet-router:
|
||||
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
|
||||
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{ROUTES}};$(ROUTES);g" | kubectl create -f-
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" | kubectl create -f-
|
||||
|
||||
@@ -15,19 +15,12 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
|
||||
AUTH_KEY: tskey-...
|
||||
```
|
||||
|
||||
1. Build and push the container
|
||||
|
||||
```bash
|
||||
export IMAGE_TAG=tailscale-k8s:latest
|
||||
make push
|
||||
```
|
||||
|
||||
1. Tailscale (v1.16+) supports storing state inside a Kubernetes Secret.
|
||||
|
||||
Configure RBAC to allow the Tailscale pod to read/write the `tailscale` secret.
|
||||
```bash
|
||||
export SA_NAME=tailscale
|
||||
export KUBE_SECRET=tailscale-auth
|
||||
export TS_KUBE_SECRET=tailscale-auth
|
||||
make rbac
|
||||
```
|
||||
|
||||
@@ -82,11 +75,11 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
|
||||
```bash
|
||||
kubectl create deployment nginx --image nginx
|
||||
kubectl expose deployment nginx --port 80
|
||||
export DEST_IP="$(kubectl get svc nginx -o=jsonpath='{.spec.clusterIP}')"
|
||||
export TS_DEST_IP="$(kubectl get svc nginx -o=jsonpath='{.spec.clusterIP}')"
|
||||
```
|
||||
**Using an existing service**
|
||||
```bash
|
||||
export DEST_IP="$(kubectl get svc <SVC_NAME> -o=jsonpath='{.spec.clusterIP}')"
|
||||
export TS_DEST_IP="$(kubectl get svc <SVC_NAME> -o=jsonpath='{.spec.clusterIP}')"
|
||||
```
|
||||
|
||||
1. Deploy the proxy pod
|
||||
@@ -114,12 +107,12 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
|
||||
Running a Tailscale [subnet router](https://tailscale.com/kb/1019/subnets/) allows you to access
|
||||
the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tailscale.
|
||||
|
||||
1. Identify the Pod/Service CIDRs that cover your Kubernetes cluster. These will vary depending on [which CNI](https://kubernetes.io/docs/concepts/cluster-administration/networking/) you are using and on the Cloud Provider you are using. Add these to the `ROUTES` variable as comma-separated values.
|
||||
1. Identify the Pod/Service CIDRs that cover your Kubernetes cluster. These will vary depending on [which CNI](https://kubernetes.io/docs/concepts/cluster-administration/networking/) you are using and on the Cloud Provider you are using. Add these to the `TS_ROUTES` variable as comma-separated values.
|
||||
|
||||
```bash
|
||||
SERVICE_CIDR=10.20.0.0/16
|
||||
POD_CIDR=10.42.0.0/15
|
||||
export ROUTES=$SERVICE_CIDR,$POD_CIDR
|
||||
export TS_ROUTES=$SERVICE_CIDR,$POD_CIDR
|
||||
```
|
||||
|
||||
1. Deploy the subnet-router pod.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
apiVersion: v1
|
||||
@@ -26,21 +26,21 @@ spec:
|
||||
containers:
|
||||
- name: tailscale
|
||||
imagePullPolicy: Always
|
||||
image: "{{IMAGE_TAG}}"
|
||||
image: "ghcr.io/tailscale/tailscale:latest"
|
||||
env:
|
||||
# Store the state in a k8s secret
|
||||
- name: KUBE_SECRET
|
||||
value: "{{KUBE_SECRET}}"
|
||||
- name: USERSPACE
|
||||
- name: TS_KUBE_SECRET
|
||||
value: "{{TS_KUBE_SECRET}}"
|
||||
- name: TS_USERSPACE
|
||||
value: "false"
|
||||
- name: AUTH_KEY
|
||||
- name: TS_AUTH_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
optional: true
|
||||
- name: DEST_IP
|
||||
value: "{{DEST_IP}}"
|
||||
- name: TS_DEST_IP
|
||||
value: "{{TS_DEST_IP}}"
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@@ -11,6 +11,6 @@ rules:
|
||||
# Create can not be restricted to a resource name.
|
||||
verbs: ["create"]
|
||||
- apiGroups: [""] # "" indicates the core API group
|
||||
resourceNames: ["{{KUBE_SECRET}}"]
|
||||
resourceNames: ["{{TS_KUBE_SECRET}}"]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "update"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
|
||||
@@ -6,19 +6,29 @@
|
||||
|
||||
export PATH=$PATH:/tailscale/bin
|
||||
|
||||
AUTH_KEY="${AUTH_KEY:-}"
|
||||
ROUTES="${ROUTES:-}"
|
||||
DEST_IP="${DEST_IP:-}"
|
||||
EXTRA_ARGS="${EXTRA_ARGS:-}"
|
||||
USERSPACE="${USERSPACE:-true}"
|
||||
KUBE_SECRET="${KUBE_SECRET:-tailscale}"
|
||||
TS_AUTH_KEY="${TS_AUTH_KEY:-}"
|
||||
TS_ROUTES="${TS_ROUTES:-}"
|
||||
TS_DEST_IP="${TS_DEST_IP:-}"
|
||||
TS_EXTRA_ARGS="${TS_EXTRA_ARGS:-}"
|
||||
TS_USERSPACE="${TS_USERSPACE:-true}"
|
||||
TS_STATE_DIR="${TS_STATE_DIR:-}"
|
||||
TS_ACCEPT_DNS="${TS_ACCEPT_DNS:-false}"
|
||||
TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}"
|
||||
|
||||
set -e
|
||||
|
||||
TAILSCALED_ARGS="--state=kube:${KUBE_SECRET} --socket=/tmp/tailscaled.sock"
|
||||
TAILSCALED_ARGS="--socket=/tmp/tailscaled.sock"
|
||||
|
||||
if [[ "${USERSPACE}" == "true" ]]; then
|
||||
if [[ ! -z "${DEST_IP}" ]]; then
|
||||
if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET}"
|
||||
elif [[ ! -z "${TS_STATE_DIR}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --statedir=${TS_STATE_DIR}"
|
||||
else
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=mem:"
|
||||
fi
|
||||
|
||||
if [[ "${TS_USERSPACE}" == "true" ]]; then
|
||||
if [[ ! -z "${TS_DEST_IP}" ]]; then
|
||||
echo "IP forwarding is not supported in userspace mode"
|
||||
exit 1
|
||||
fi
|
||||
@@ -37,23 +47,23 @@ echo "Starting tailscaled"
|
||||
tailscaled ${TAILSCALED_ARGS} &
|
||||
PID=$!
|
||||
|
||||
UP_ARGS="--accept-dns=false"
|
||||
if [[ ! -z "${ROUTES}" ]]; then
|
||||
UP_ARGS="--advertise-routes=${ROUTES} ${UP_ARGS}"
|
||||
UP_ARGS="--accept-dns=${TS_ACCEPT_DNS}"
|
||||
if [[ ! -z "${TS_ROUTES}" ]]; then
|
||||
UP_ARGS="--advertise-routes=${TS_ROUTES} ${UP_ARGS}"
|
||||
fi
|
||||
if [[ ! -z "${AUTH_KEY}" ]]; then
|
||||
UP_ARGS="--authkey=${AUTH_KEY} ${UP_ARGS}"
|
||||
if [[ ! -z "${TS_AUTH_KEY}" ]]; then
|
||||
UP_ARGS="--authkey=${TS_AUTH_KEY} ${UP_ARGS}"
|
||||
fi
|
||||
if [[ ! -z "${EXTRA_ARGS}" ]]; then
|
||||
UP_ARGS="${UP_ARGS} ${EXTRA_ARGS:-}"
|
||||
if [[ ! -z "${TS_EXTRA_ARGS}" ]]; then
|
||||
UP_ARGS="${UP_ARGS} ${TS_EXTRA_ARGS:-}"
|
||||
fi
|
||||
|
||||
echo "Running tailscale up"
|
||||
tailscale --socket=/tmp/tailscaled.sock up ${UP_ARGS}
|
||||
|
||||
if [[ ! -z "${DEST_IP}" ]]; then
|
||||
if [[ ! -z "${TS_DEST_IP}" ]]; then
|
||||
echo "Adding iptables rule for DNAT"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${DEST_IP}"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
fi
|
||||
|
||||
wait ${PID}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
apiVersion: v1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
apiVersion: v1
|
||||
@@ -12,14 +12,14 @@ spec:
|
||||
image: nginx
|
||||
- name: ts-sidecar
|
||||
imagePullPolicy: Always
|
||||
image: "{{IMAGE_TAG}}"
|
||||
image: "ghcr.io/tailscale/tailscale:latest"
|
||||
env:
|
||||
# Store the state in a k8s secret
|
||||
- name: KUBE_SECRET
|
||||
value: "{{KUBE_SECRET}}"
|
||||
- name: USERSPACE
|
||||
- name: TS_KUBE_SECRET
|
||||
value: "{{TS_KUBE_SECRET}}"
|
||||
- name: TS_USERSPACE
|
||||
value: "false"
|
||||
- name: AUTH_KEY
|
||||
- name: TS_AUTH_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
apiVersion: v1
|
||||
@@ -12,21 +12,21 @@ spec:
|
||||
containers:
|
||||
- name: tailscale
|
||||
imagePullPolicy: Always
|
||||
image: "{{IMAGE_TAG}}"
|
||||
image: "ghcr.io/tailscale/tailscale:latest"
|
||||
env:
|
||||
# Store the state in a k8s secret
|
||||
- name: KUBE_SECRET
|
||||
value: "{{KUBE_SECRET}}"
|
||||
- name: USERSPACE
|
||||
- name: TS_KUBE_SECRET
|
||||
value: "{{TS_KUBE_SECRET}}"
|
||||
- name: TS_USERSPACE
|
||||
value: "true"
|
||||
- name: AUTH_KEY
|
||||
- name: TS_AUTH_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
optional: true
|
||||
- name: ROUTES
|
||||
value: "{{ROUTES}}"
|
||||
- name: TS_ROUTES
|
||||
value: "{{TS_ROUTES}}"
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# 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.
|
||||
apiVersion: v1
|
||||
@@ -12,17 +12,17 @@ spec:
|
||||
image: nginx
|
||||
- name: ts-sidecar
|
||||
imagePullPolicy: Always
|
||||
image: "{{IMAGE_TAG}}"
|
||||
image: "ghcr.io/tailscale/tailscale:latest"
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
env:
|
||||
# Store the state in a k8s secret
|
||||
- name: KUBE_SECRET
|
||||
value: "{{KUBE_SECRET}}"
|
||||
- name: USERSPACE
|
||||
- name: TS_KUBE_SECRET
|
||||
value: "{{TS_KUBE_SECRET}}"
|
||||
- name: TS_USERSPACE
|
||||
value: "true"
|
||||
- name: AUTH_KEY
|
||||
- name: TS_AUTH_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
|
||||
32
go.mod
32
go.mod
@@ -20,14 +20,14 @@ require (
|
||||
github.com/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/google/go-cmp v0.5.7
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e
|
||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.13.6
|
||||
github.com/klauspost/compress v1.15.4
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
|
||||
github.com/mdlayher/genetlink v1.2.0
|
||||
github.com/mdlayher/netlink v1.6.0
|
||||
@@ -42,7 +42,8 @@ require (
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
|
||||
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
@@ -50,9 +51,9 @@ require (
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1
|
||||
@@ -60,7 +61,7 @@ require (
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
@@ -76,7 +77,7 @@ require (
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
@@ -106,9 +107,14 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/charithe/durationcheck v0.0.9 // indirect
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
github.com/docker/cli v20.10.16+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.16+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/esimonov/ifshort v1.0.3 // indirect
|
||||
github.com/ettle/strcase v0.1.1 // indirect
|
||||
@@ -143,6 +149,7 @@ require (
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/go-containerregistry v0.9.0 // indirect
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
|
||||
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
|
||||
@@ -198,6 +205,8 @@ require (
|
||||
github.com/nishanths/predeclared v0.2.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
|
||||
@@ -225,7 +234,7 @@ require (
|
||||
github.com/sourcegraph/go-diff v0.6.1 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.2.1 // indirect
|
||||
github.com/spf13/cobra v1.4.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.9.0 // indirect
|
||||
@@ -244,17 +253,18 @@ require (
|
||||
github.com/ultraware/funlen v0.0.3 // indirect
|
||||
github.com/ultraware/whitespace v0.0.4 // indirect
|
||||
github.com/uudashr/gocognit v1.0.5 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/yeya24/promlinter v0.1.0 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
69
go.sum
69
go.sum
@@ -83,8 +83,8 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB
|
||||
github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
|
||||
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
||||
@@ -210,6 +210,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4 h1:LjrYUZpyOhiSaU7hHrdR82/RBoxfGWSaC0VeSSMXqnk=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4/go.mod h1:7vRJIcImfY8bpifnMjt+HTJoQxASq7T28MYbP15/Nf0=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@@ -229,6 +231,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
@@ -237,6 +240,7 @@ github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD
|
||||
github.com/daixiang0/gci v0.2.7/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
|
||||
github.com/daixiang0/gci v0.2.9 h1:iwJvwQpBZmMg31w+QQ6jsyZ54KEATn6/nfARbBNW294=
|
||||
github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
|
||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -248,6 +252,14 @@ github.com/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7q
|
||||
github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/cli v20.10.16+incompatible h1:aLQ8XowgKpR3/IysPj8qZQJBVQ+Qws61icFuZl6iKYs=
|
||||
github.com/docker/cli v20.10.16+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
|
||||
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405crbpxvWzKovETOQ=
|
||||
github.com/docker/docker v20.10.16+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
|
||||
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
@@ -467,8 +479,11 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.9.0 h1:5Ths7RjxyFV0huKChQTgY6fLzvHhZMpLTFNja8U0/0w=
|
||||
github.com/google/go-containerregistry v0.9.0/go.mod h1:9eq4BnSufyT1kHNffX+vSXVonaJ7yaIOulrKZejMxnQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4=
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
|
||||
@@ -675,8 +690,9 @@ github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ=
|
||||
github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -869,6 +885,10 @@ github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 h1:+czc/J8SlhPKLOtVLMQc+xDCFBT73ZStMsRhSsUhsSg=
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198/go.mod h1:j4h1pJW6ZcJTgMZWP3+7RlG3zTaP02aDZ/Qw0sppK7Q=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
@@ -965,7 +985,9 @@ github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6
|
||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM=
|
||||
github.com/ryancurrah/gomodguard v1.2.3 h1:ww2fsjqocGCAFamzvv/b8IsRduuHHeK2MHTcTxZTQX8=
|
||||
github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg=
|
||||
@@ -1026,8 +1048,9 @@ github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
@@ -1071,8 +1094,10 @@ github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 h1:f7nwzdAHTUUOJjHZuDvLz9CEAlUM228amCRvwzlPvsA=
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83/go.mod h1:iTDXJsA6A2wNNjurgic2rk+is6uzU4U2NLm4T+edr6M=
|
||||
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17 h1:QaQrUggZ7U2lE3HhoPx6bDK7fO385FR7pHRYSPEv70Q=
|
||||
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/dIMvD0t77g0FxFfZIQjghDQxyG2U=
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
@@ -1128,6 +1153,7 @@ github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFO
|
||||
github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
|
||||
github.com/uudashr/gocognit v1.0.5 h1:rrSex7oHr3/pPLQ0xoWq108XMU8s678FJcQ+aSfOHa4=
|
||||
github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA=
|
||||
@@ -1138,6 +1164,8 @@ github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD
|
||||
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
|
||||
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
|
||||
github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
@@ -1148,6 +1176,9 @@ github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0B
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
|
||||
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
@@ -1200,8 +1231,9 @@ go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofe
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1338,8 +1370,8 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1370,8 +1402,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1478,8 +1511,8 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
@@ -1764,8 +1797,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1804,6 +1838,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445 h1:pLNQCtMzh4O6rdhoUeWHuutt4yMft+B9Cgw/bezWchE=
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1821,8 +1856,8 @@ howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCU
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310 h1:0jKHTf+W75kYRyg5bto1UT+r18QmAz2u/5pAs/fx4zo=
|
||||
|
||||
@@ -1 +1 @@
|
||||
710a0d861098c07540ad073bb73a42ce81bf54a8
|
||||
04d67b90d8cfd6f822664220f79e0e69cacb6b5c
|
||||
|
||||
@@ -45,6 +45,7 @@ var (
|
||||
anyInterfaceUp = true // until told otherwise
|
||||
udp4Unbound bool
|
||||
controlHealth []string
|
||||
lastLoginErr error
|
||||
)
|
||||
|
||||
// Subsystem is the name of a subsystem whose health can be monitored.
|
||||
@@ -287,6 +288,15 @@ func SetUDP4Unbound(unbound bool) {
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetAuthRoutineInError records the latest error encountered as a result of a
|
||||
// login attempt. Providing a nil error indicates successful login, or that
|
||||
// being logged in w/coordination is not currently desired.
|
||||
func SetAuthRoutineInError(err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lastLoginErr = err
|
||||
}
|
||||
|
||||
func timerSelfCheck() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -321,9 +331,12 @@ func overallErrorLocked() error {
|
||||
if !anyInterfaceUp {
|
||||
return errors.New("network down")
|
||||
}
|
||||
if ipnState != "Running" || !ipnWantRunning {
|
||||
if !ipnWantRunning {
|
||||
return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning)
|
||||
}
|
||||
if lastLoginErr != nil {
|
||||
return fmt.Errorf("not logged in, last login error=%v", lastLoginErr)
|
||||
}
|
||||
now := time.Now()
|
||||
if !inMapPoll && (lastMapPollEndedAt.IsZero() || now.Sub(lastMapPollEndedAt) > 10*time.Second) {
|
||||
return errors.New("not in map poll")
|
||||
|
||||
@@ -27,12 +27,7 @@ func osVersionFreebsd() string {
|
||||
|
||||
var attrBuf strings.Builder
|
||||
attrBuf.WriteString("; version=")
|
||||
for _, b := range un.Release {
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
attrBuf.WriteString(unix.ByteSliceToString(un.Release[:]))
|
||||
attr := attrBuf.String()
|
||||
|
||||
version := "FreeBSD"
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -37,6 +37,7 @@ func linuxDeviceModel() string {
|
||||
// Otherwise, try the Devicetree model, usually set on
|
||||
// ARM SBCs, etc.
|
||||
// Example: "Raspberry Pi 4 Model B Rev 1.2"
|
||||
// Example: "WD My Cloud Gen2: Marvell Armada 375"
|
||||
"/sys/firmware/devicetree/base/model", // Raspberry Pi 4 Model B Rev 1.2"
|
||||
} {
|
||||
b, _ := os.ReadFile(path)
|
||||
@@ -55,6 +56,9 @@ func osVersionLinux() string {
|
||||
propFile = "/etc.defaults/VERSION"
|
||||
case distro.OpenWrt:
|
||||
propFile = "/etc/openwrt_release"
|
||||
case distro.WDMyCloud:
|
||||
slurp, _ := ioutil.ReadFile("/etc/version")
|
||||
return fmt.Sprintf("%s", string(bytes.TrimSpace(slurp)))
|
||||
}
|
||||
|
||||
m := map[string]string{}
|
||||
@@ -68,17 +72,12 @@ func osVersionLinux() string {
|
||||
return nil
|
||||
})
|
||||
|
||||
var un syscall.Utsname
|
||||
syscall.Uname(&un)
|
||||
var un unix.Utsname
|
||||
unix.Uname(&un)
|
||||
|
||||
var attrBuf strings.Builder
|
||||
attrBuf.WriteString("; kernel=")
|
||||
for _, b := range un.Release {
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
attrBuf.WriteString(unix.ByteSliceToString(un.Release[:]))
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
|
||||
package ipn
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
nm: &netmap.NetworkMap{},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
},
|
||||
},
|
||||
@@ -77,7 +77,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"b.net.": ips("100.102.0.1", "100.102.0.2"),
|
||||
"myname.net.": ips("100.101.101.101"),
|
||||
@@ -112,7 +112,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
OnlyIPv6: true,
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"b.net.": ips("fe75::2"),
|
||||
"myname.net.": ips("fe75::1"),
|
||||
@@ -136,7 +136,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"myname.net.": ips("100.101.101.101"),
|
||||
"foo.com.": ips("1.2.3.4"),
|
||||
@@ -158,7 +158,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
"0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.": nil,
|
||||
"100.100.in-addr.arpa.": nil,
|
||||
"101.100.in-addr.arpa.": nil,
|
||||
@@ -242,13 +242,13 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
Resolvers: []dnstype.Resolver{
|
||||
Resolvers: []*dnstype.Resolver{
|
||||
{Addr: "8.8.8.8"},
|
||||
},
|
||||
FallbackResolvers: []dnstype.Resolver{
|
||||
FallbackResolvers: []*dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
Routes: map[string][]dnstype.Resolver{
|
||||
Routes: map[string][]*dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
||||
},
|
||||
},
|
||||
@@ -258,10 +258,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
DefaultResolvers: []*dnstype.Resolver{
|
||||
{Addr: "8.8.8.8"},
|
||||
},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
||||
},
|
||||
},
|
||||
@@ -270,7 +270,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
name: "exit_nodes_need_fallbacks",
|
||||
nm: &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
FallbackResolvers: []dnstype.Resolver{
|
||||
FallbackResolvers: []*dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
},
|
||||
@@ -281,8 +281,8 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
DefaultResolvers: []*dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
},
|
||||
@@ -291,7 +291,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
name: "not_exit_node_NOT_need_fallbacks",
|
||||
nm: &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
FallbackResolvers: []dnstype.Resolver{
|
||||
FallbackResolvers: []*dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
},
|
||||
@@ -301,7 +301,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ type SSHServer interface {
|
||||
// so that existing sessions can be re-evaluated for validity
|
||||
// and closed if they'd no longer be accepted.
|
||||
OnPolicyChange()
|
||||
|
||||
// Shutdown is called when tailscaled is shutting down.
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
type newSSHServerFunc func(logger.Logf, *LocalBackend) (SSHServer, error)
|
||||
@@ -122,8 +125,7 @@ type LocalBackend struct {
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
sshAtomicBool syncs.AtomicBool
|
||||
sshServer SSHServer // or nil
|
||||
shutdownCalled bool // if Shutdown has been called
|
||||
shutdownCalled bool // if Shutdown has been called
|
||||
|
||||
filterAtomic atomic.Value // of *filter.Filter
|
||||
containsViaIPFuncAtomic atomic.Value // of func(netaddr.IP) bool
|
||||
@@ -133,6 +135,7 @@ type LocalBackend struct {
|
||||
filterHash deephash.Sum
|
||||
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
|
||||
ccGen clientGen // function for producing controlclient; lazily populated
|
||||
sshServer SSHServer // or nil, initialized lazily.
|
||||
notify func(ipn.Notify)
|
||||
cc controlclient.Client
|
||||
stateKey ipn.StateKey // computed in part from user-provided value
|
||||
@@ -225,12 +228,6 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
loginFlags: loginFlags,
|
||||
}
|
||||
if newSSHServer != nil {
|
||||
b.sshServer, err = newSSHServer(logf, b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newSSHServer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
|
||||
@@ -346,6 +343,10 @@ func (b *LocalBackend) Shutdown() {
|
||||
b.mu.Lock()
|
||||
b.shutdownCalled = true
|
||||
cc := b.cc
|
||||
if b.sshServer != nil {
|
||||
b.sshServer.Shutdown()
|
||||
b.sshServer = nil
|
||||
}
|
||||
b.closePeerAPIListenersLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -415,6 +416,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
s.Health = append(s.Health, err.Error())
|
||||
}
|
||||
}
|
||||
if m := b.sshOnButUnusableHealthCheckMessageLocked(); m != "" {
|
||||
s.Health = append(s.Health, m)
|
||||
}
|
||||
if b.netMap != nil {
|
||||
s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...)
|
||||
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
@@ -955,6 +959,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.logf("failed to save UpdatePrefs state: %v", err)
|
||||
}
|
||||
}
|
||||
b.setAtomicValuesFromPrefs(b.prefs)
|
||||
}
|
||||
|
||||
wantRunning := b.prefs.WantRunning
|
||||
@@ -1033,7 +1038,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
DiscoPublicKey: discoPublic,
|
||||
DebugFlags: debugFlags,
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b.e,
|
||||
Pinger: b,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
Dialer: b.Dialer(),
|
||||
|
||||
@@ -1699,6 +1704,27 @@ func (b *LocalBackend) StartLoginInteractive() {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Ping(ctx context.Context, ip netaddr.IP, pingType tailcfg.PingType) (*ipnstate.PingResult, error) {
|
||||
if pingType == tailcfg.PingPeerAPI {
|
||||
t0 := time.Now()
|
||||
node, base, err := b.pingPeerAPI(ctx, ip)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
d := time.Since(t0)
|
||||
pr := &ipnstate.PingResult{
|
||||
IP: ip.String(),
|
||||
NodeIP: ip.String(),
|
||||
LatencySeconds: d.Seconds(),
|
||||
PeerAPIURL: base,
|
||||
}
|
||||
if err != nil {
|
||||
pr.Err = err.Error()
|
||||
}
|
||||
if node != nil {
|
||||
pr.NodeName = node.Name
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
b.e.Ping(ip, pingType, func(pr *ipnstate.PingResult) {
|
||||
select {
|
||||
@@ -1714,6 +1740,37 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netaddr.IP, pingType tailcfg
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netaddr.IP) (peer *tailcfg.Node, peerBase string, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
nm := b.NetMap()
|
||||
if nm == nil {
|
||||
return nil, "", errors.New("no netmap")
|
||||
}
|
||||
peer, ok := nm.PeerByTailscaleIP(ip)
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("no peer found with Tailscale IP %v", ip)
|
||||
}
|
||||
base := peerAPIBase(nm, peer)
|
||||
if base == "" {
|
||||
return nil, "", fmt.Errorf("no peer API base found for peer %v (%v)", peer.ID, ip)
|
||||
}
|
||||
outReq, err := http.NewRequestWithContext(ctx, "HEAD", base, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
tr := b.Dialer().PeerAPITransport()
|
||||
res, err := tr.RoundTrip(outReq)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer res.Body.Close() // but unnecessary on HEAD responses
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, "", fmt.Errorf("HTTP status %v", res.Status)
|
||||
}
|
||||
return peer, base, nil
|
||||
}
|
||||
|
||||
// parseWgStatusLocked returns an EngineStatus based on s.
|
||||
//
|
||||
// b.mu must be held; mostly because the caller is about to anyway, and doing so
|
||||
@@ -1772,36 +1829,88 @@ func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
var errs []error
|
||||
if p.Hostname == "badhostname.tailscale." {
|
||||
// Keep this one just for testing.
|
||||
return errors.New("bad hostname [test]")
|
||||
errs = append(errs, errors.New("bad hostname [test]"))
|
||||
}
|
||||
if p.RunSSH {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
if err := b.checkSSHPrefsLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
if !p.RunSSH {
|
||||
return nil
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on Synology.")
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if b.netMap != nil && b.netMap.SSHPolicy == nil &&
|
||||
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.")
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
return nil
|
||||
}
|
||||
if b.netMap != nil {
|
||||
if !hasCapability(b.netMap, tailcfg.CapabilitySSH) {
|
||||
if b.isDefaultServerLocked() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet. See https://tailscale.com/s/ssh")
|
||||
}
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
|
||||
if b.prefs == nil || !b.prefs.RunSSH {
|
||||
return ""
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
return "development SSH policy in use"
|
||||
}
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return ""
|
||||
}
|
||||
if nm.SSHPolicy != nil && len(nm.SSHPolicy.Rules) > 0 {
|
||||
return ""
|
||||
}
|
||||
isDefault := b.isDefaultServerLocked()
|
||||
isAdmin := hasCapability(nm, tailcfg.CapabilityAdmin)
|
||||
|
||||
if !isAdmin {
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access."
|
||||
}
|
||||
if !isDefault {
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs to allow access."
|
||||
}
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs at https://tailscale.com/s/ssh-policy"
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isDefaultServerLocked() bool {
|
||||
if b.prefs == nil {
|
||||
return true // assume true until set otherwise
|
||||
}
|
||||
return b.prefs.ControlURLOrDefault() == ipn.DefaultControlURL
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
b.mu.Lock()
|
||||
p0 := b.prefs.Clone()
|
||||
@@ -1873,6 +1982,12 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
}
|
||||
b.updateFilterLocked(netMap, newp)
|
||||
|
||||
if oldp.ShouldSSHBeRunning() && !newp.ShouldSSHBeRunning() {
|
||||
if b.sshServer != nil {
|
||||
go b.sshServer.Shutdown()
|
||||
b.sshServer = nil
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if stateKey != "" {
|
||||
@@ -1916,10 +2031,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
if oldp.RunSSH && !newp.RunSSH && b.sshServer != nil {
|
||||
go b.sshServer.OnPolicyChange()
|
||||
}
|
||||
|
||||
b.send(ipn.Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
@@ -2112,7 +2223,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
// a runtime.GOOS.
|
||||
func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Logf, versionOS string) *dns.Config {
|
||||
dcfg := &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
}
|
||||
|
||||
@@ -2199,8 +2310,9 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
}
|
||||
}
|
||||
|
||||
addDefault := func(resolvers []dnstype.Resolver) {
|
||||
addDefault := func(resolvers []*dnstype.Resolver) {
|
||||
for _, r := range resolvers {
|
||||
r := r
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, r)
|
||||
}
|
||||
}
|
||||
@@ -2208,7 +2320,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
// If we're using an exit node and that exit node is new enough (1.19.x+)
|
||||
// to run a DoH DNS proxy, then send all our DNS traffic through it.
|
||||
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
|
||||
addDefault([]dnstype.Resolver{{Addr: dohURL}})
|
||||
addDefault([]*dnstype.Resolver{{Addr: dohURL}})
|
||||
return dcfg
|
||||
}
|
||||
|
||||
@@ -2227,7 +2339,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
//
|
||||
// While we're already populating it, might as well size the
|
||||
// slice appropriately.
|
||||
dcfg.Routes[fqdn] = make([]dnstype.Resolver, 0, len(resolvers))
|
||||
dcfg.Routes[fqdn] = make([]*dnstype.Resolver, 0, len(resolvers))
|
||||
|
||||
for _, r := range resolvers {
|
||||
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], r)
|
||||
@@ -2623,12 +2735,14 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
b.maybePauseControlClientLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
// prefs may change irrespective of state; WantRunning should be explicitly
|
||||
// set before potential early return even if the state is unchanged.
|
||||
health.SetIPNState(newState.String(), prefs.WantRunning)
|
||||
if oldState == newState {
|
||||
return
|
||||
}
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)",
|
||||
oldState, newState, prefs.WantRunning, netMap != nil)
|
||||
health.SetIPNState(newState.String(), prefs.WantRunning)
|
||||
b.send(ipn.Notify{State: &newState})
|
||||
|
||||
switch newState {
|
||||
@@ -3307,11 +3421,28 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
return cc.DoNoiseRequest(req)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleSSHConn(c net.Conn) error {
|
||||
if b.sshServer == nil {
|
||||
return errors.New("no SSH server")
|
||||
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.sshServer != nil {
|
||||
return b.sshServer, nil
|
||||
}
|
||||
return b.sshServer.HandleSSHConn(c)
|
||||
if newSSHServer == nil {
|
||||
return nil, errors.New("no SSH server support")
|
||||
}
|
||||
b.sshServer, err = newSSHServer(b.logf, b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newSSHServer: %w", err)
|
||||
}
|
||||
return b.sshServer, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleSSHConn(c net.Conn) (err error) {
|
||||
s, err := b.sshServerOrInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.HandleSSHConn(c)
|
||||
}
|
||||
|
||||
// HandleQuad100Port80Conn serves http://100.100.100.100/ on port 80 (and
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
//go:build !ios && !android && !js
|
||||
// +build !ios,!android,!js
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
|
||||
17
ipn/ipnserver/proxyconnect_js.go
Normal file
17
ipn/ipnserver/proxyconnect_js.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 ipnserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) {
|
||||
panic("unreachable")
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
// Options is the configuration of the Tailscale node agent.
|
||||
@@ -659,7 +660,7 @@ func (s *Server) writeToClients(n ipn.Notify) {
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
//
|
||||
// Deprecated: use New and Server.Run instead.
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, *netstack.Impl, error), opts Options) error {
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
@@ -706,7 +707,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
eng, ns, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
@@ -717,7 +718,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
continue
|
||||
}
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
eng, ns, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
@@ -747,6 +748,9 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ns != nil {
|
||||
ns.SetLocalBackend(server.LocalBackend())
|
||||
}
|
||||
serverMu.Lock()
|
||||
serverOrNil = server
|
||||
serverMu.Unlock()
|
||||
@@ -996,29 +1000,26 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
|
||||
// FixedEngine returns a func that returns eng and a nil error.
|
||||
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
// getEngineUntilItWorksWrapper returns a getEngine wrapper that does
|
||||
// not call getEngine concurrently and stops calling getEngine once
|
||||
// it's returned a working engine.
|
||||
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) func() (wgengine.Engine, error) {
|
||||
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, *netstack.Impl, error)) func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
var mu sync.Mutex
|
||||
var engGood wgengine.Engine
|
||||
return func() (wgengine.Engine, error) {
|
||||
var nsGood *netstack.Impl
|
||||
return func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if engGood != nil {
|
||||
return engGood, nil
|
||||
return engGood, nsGood, nil
|
||||
}
|
||||
e, err := getEngine()
|
||||
e, ns, err := getEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
engGood = e
|
||||
return e, nil
|
||||
nsGood = ns
|
||||
return e, ns, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
func TestRunMultipleAccepts(t *testing.T) {
|
||||
@@ -75,6 +76,11 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
// FixedEngine returns a func that returns eng and a nil error.
|
||||
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
return func() (wgengine.Engine, *netstack.Impl, error) { return eng, nil, nil }
|
||||
}
|
||||
|
||||
@@ -508,6 +508,10 @@ type PingResult struct {
|
||||
// running the server on.
|
||||
PeerAPIPort uint16 `json:",omitempty"`
|
||||
|
||||
// PeerAPIURL is the URL that was hit for pings of type "peerapi" (tailcfg.PingPeerAPI).
|
||||
// It's of the form "http://ip:port" (or [ip]:port for IPv6).
|
||||
PeerAPIURL string `json:",omitempty"`
|
||||
|
||||
// IsLocalIP is whether the ping request error is due to it being
|
||||
// a ping to the local node.
|
||||
IsLocalIP bool `json:",omitempty"`
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
//go:build !ios && !android && !js
|
||||
// +build !ios,!android,!js
|
||||
|
||||
package localapi
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ios || android
|
||||
// +build ios android
|
||||
//go:build ios || android || js
|
||||
// +build ios android js
|
||||
|
||||
package localapi
|
||||
|
||||
|
||||
@@ -109,6 +109,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveStatus(w, r)
|
||||
case "/localapi/v0/logout":
|
||||
h.serveLogout(w, r)
|
||||
case "/localapi/v0/login-interactive":
|
||||
h.serveLoginInteractive(w, r)
|
||||
case "/localapi/v0/prefs":
|
||||
h.servePrefs(w, r)
|
||||
case "/localapi/v0/ping":
|
||||
@@ -343,6 +345,20 @@ func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
|
||||
e.Encode(st)
|
||||
}
|
||||
|
||||
func (h *Handler) serveLoginInteractive(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "login access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
h.b.StartLoginInteractive()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "logout access denied", http.StatusForbidden)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
//go:build !ios && !android && !js
|
||||
// +build !ios,!android,!js
|
||||
|
||||
// We don't include it on mobile where we're more memory constrained and
|
||||
// there's no CLI to get at the results anyway.
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs
|
||||
|
||||
// DefaultControlURL is the URL base of the control plane
|
||||
// ("coordination server") for use when no explicit one is configured.
|
||||
@@ -584,6 +584,12 @@ func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ShouldSSHBeRunning reports whether the SSH server should be running based on
|
||||
// the prefs.
|
||||
func (p *Prefs) ShouldSSHBeRunning() bool {
|
||||
return p.WantRunning && p.RunSSH
|
||||
}
|
||||
|
||||
// PrefsFromBytes deserializes Prefs from a JSON blob.
|
||||
func PrefsFromBytes(b []byte) (*Prefs, error) {
|
||||
p := NewPrefs()
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"tailscale.com/util/racebuild"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var getLogTargetOnce struct {
|
||||
@@ -519,6 +520,8 @@ func New(collection string) *Policy {
|
||||
}
|
||||
if collection == logtail.CollectionNode {
|
||||
c.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
|
||||
c.IncludeProcID = true
|
||||
c.IncludeProcSequence = true
|
||||
}
|
||||
|
||||
if val := getLogTarget(); val != "" {
|
||||
@@ -528,9 +531,25 @@ func New(collection string) *Policy {
|
||||
c.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host)}
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{
|
||||
filchOptions := filch.Options{
|
||||
ReplaceStderr: redirectStderrToLogPanics(),
|
||||
})
|
||||
}
|
||||
filchPrefix := filepath.Join(dir, cmdName)
|
||||
|
||||
// Synology disks cannot hibernate if we're writing logs to them all the time.
|
||||
// https://github.com/tailscale/tailscale/issues/3551
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
synologyTmpfsLogs := "/tmp/tailscale-logs"
|
||||
if err := os.MkdirAll(synologyTmpfsLogs, 0755); err == nil {
|
||||
filchPrefix = filepath.Join(synologyTmpfsLogs, cmdName)
|
||||
filchOptions.MaxFileSize = 1 << 20
|
||||
} else {
|
||||
// not a fatal error, we can leave the log files on the spinning disk
|
||||
log.Printf("Unable to create /tmp directory for log storage: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filchPrefix, filchOptions)
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
if filchBuf.OrigStderr != nil {
|
||||
|
||||
@@ -8,6 +8,8 @@ package logtail
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -62,6 +65,17 @@ type Config struct {
|
||||
// DrainLogs, if non-nil, disables automatic uploading of new logs,
|
||||
// so that logs are only uploaded when a token is sent to DrainLogs.
|
||||
DrainLogs <-chan struct{}
|
||||
|
||||
// IncludeProcID, if true, results in an ephemeral process identifier being
|
||||
// included in logs. The ID is random and not guaranteed to be globally
|
||||
// unique, but it can be used to distinguish between different instances
|
||||
// running with same PrivateID.
|
||||
IncludeProcID bool
|
||||
|
||||
// IncludeProcSequence, if true, results in an ephemeral sequence number
|
||||
// being included in the logs. The sequence number is incremented for each
|
||||
// log message sent, but is not peristed across process restarts.
|
||||
IncludeProcSequence bool
|
||||
}
|
||||
|
||||
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
@@ -84,6 +98,17 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
}
|
||||
cfg.Buffer = NewMemoryBuffer(pendingSize)
|
||||
}
|
||||
var procID uint32
|
||||
if cfg.IncludeProcID {
|
||||
keyBytes := make([]byte, 4)
|
||||
rand.Read(keyBytes)
|
||||
procID = binary.LittleEndian.Uint32(keyBytes)
|
||||
if procID == 0 {
|
||||
// 0 is the empty/off value, assign a different (non-zero) value to
|
||||
// make sure we still include an ID (actual value does not matter).
|
||||
procID = 7
|
||||
}
|
||||
}
|
||||
l := &Logger{
|
||||
privateID: cfg.PrivateID,
|
||||
stderr: cfg.Stderr,
|
||||
@@ -100,6 +125,9 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
bo: backoff.NewBackoff("logtail", logf, 30*time.Second),
|
||||
metricsDelta: cfg.MetricsDelta,
|
||||
|
||||
procID: procID,
|
||||
includeProcSequence: cfg.IncludeProcSequence,
|
||||
|
||||
shutdownStart: make(chan struct{}),
|
||||
shutdownDone: make(chan struct{}),
|
||||
}
|
||||
@@ -137,6 +165,11 @@ type Logger struct {
|
||||
metricsDelta func() string // or nil
|
||||
privateID PrivateID
|
||||
|
||||
procID uint32
|
||||
includeProcSequence bool
|
||||
writeLock sync.Mutex // guards increments of procSequence
|
||||
procSequence uint64
|
||||
|
||||
shutdownStart chan struct{} // closed when shutdown begins
|
||||
shutdownDone chan struct{} // closed when shutdown complete
|
||||
}
|
||||
@@ -253,8 +286,6 @@ func (l *Logger) drainPending(scratch []byte) (res []byte) {
|
||||
if b[0] != '{' || !json.Valid(b) {
|
||||
// This is probably a log added to stderr by filch
|
||||
// outside of the logtail logger. Encode it.
|
||||
// Do not add a client time, as it could have been
|
||||
// been written a long time ago.
|
||||
if !l.explainedRaw {
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: *** Lines prefixed with RAW-STDERR below bypassed logtail and probably come from a previous run of the program\n")
|
||||
@@ -263,7 +294,10 @@ func (l *Logger) drainPending(scratch []byte) (res []byte) {
|
||||
l.explainedRaw = true
|
||||
}
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: %s", b)
|
||||
b = l.encodeText(b, true, 0)
|
||||
// Do not add a client time, as it could have been
|
||||
// been written a long time ago. Don't include instance key or ID
|
||||
// either, since this came from a different instance.
|
||||
b = l.encodeText(b, true, 0, 0, 0)
|
||||
}
|
||||
|
||||
if entries > 0 {
|
||||
@@ -361,7 +395,11 @@ func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
// origlen indicates the pre-compression body length.
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
|
||||
req, err := http.NewRequest("POST", l.url, bytes.NewReader(body))
|
||||
const maxUploadTime = 45 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", l.url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
// I know of no conditions under which this could fail.
|
||||
// Report it very loudly.
|
||||
@@ -374,11 +412,6 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
|
||||
}
|
||||
req.Header["User-Agent"] = nil // not worth writing one; save some bytes
|
||||
|
||||
maxUploadTime := 45 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
|
||||
defer cancel()
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
compressedNote := "not-compressed"
|
||||
if origlen != -1 {
|
||||
compressedNote = "compressed"
|
||||
@@ -437,14 +470,24 @@ func (l *Logger) send(jsonBlob []byte) (int, error) {
|
||||
|
||||
// TODO: instead of allocating, this should probably just append
|
||||
// directly into the output log buffer.
|
||||
func (l *Logger) encodeText(buf []byte, skipClientTime bool, level int) []byte {
|
||||
func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, procSequence uint64, level int) []byte {
|
||||
now := l.timeNow()
|
||||
|
||||
// Factor in JSON encoding overhead to try to only do one alloc
|
||||
// in the make below (so appends don't resize the buffer).
|
||||
overhead := 13
|
||||
overhead := len(`{"text": ""}\n`)
|
||||
includeLogtail := !skipClientTime || procID != 0 || procSequence != 0
|
||||
if includeLogtail {
|
||||
overhead += len(`"logtail": {},`)
|
||||
}
|
||||
if !skipClientTime {
|
||||
overhead += 67
|
||||
overhead += len(`"client_time": "2006-01-02T15:04:05.999999999Z07:00",`)
|
||||
}
|
||||
if procID != 0 {
|
||||
overhead += len(`"proc_id": 4294967296,`)
|
||||
}
|
||||
if procSequence != 0 {
|
||||
overhead += len(`"proc_seq": 9007199254740992,`)
|
||||
}
|
||||
// TODO: do a pass over buf and count how many backslashes will be needed?
|
||||
// For now just factor in a dozen.
|
||||
@@ -468,10 +511,25 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, level int) []byte {
|
||||
b := make([]byte, 0, len(buf)+overhead)
|
||||
b = append(b, '{')
|
||||
|
||||
if !skipClientTime {
|
||||
b = append(b, `"logtail": {"client_time": "`...)
|
||||
b = now.AppendFormat(b, time.RFC3339Nano)
|
||||
b = append(b, "\"}, "...)
|
||||
if includeLogtail {
|
||||
b = append(b, `"logtail": {`...)
|
||||
if !skipClientTime {
|
||||
b = append(b, `"client_time": "`...)
|
||||
b = now.AppendFormat(b, time.RFC3339Nano)
|
||||
b = append(b, `",`...)
|
||||
}
|
||||
if procID != 0 {
|
||||
b = append(b, `"proc_id": `...)
|
||||
b = strconv.AppendUint(b, uint64(procID), 10)
|
||||
b = append(b, ',')
|
||||
}
|
||||
if procSequence != 0 {
|
||||
b = append(b, `"proc_seq": `...)
|
||||
b = strconv.AppendUint(b, procSequence, 10)
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = bytes.TrimRight(b, ",")
|
||||
b = append(b, "}, "...)
|
||||
}
|
||||
|
||||
if l.metricsDelta != nil {
|
||||
@@ -521,8 +579,11 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, level int) []byte {
|
||||
}
|
||||
|
||||
func (l *Logger) encode(buf []byte, level int) []byte {
|
||||
if l.includeProcSequence {
|
||||
l.procSequence++
|
||||
}
|
||||
if buf[0] != '{' {
|
||||
return l.encodeText(buf, l.skipClientTime, level) // text fast-path
|
||||
return l.encodeText(buf, l.skipClientTime, l.procID, l.procSequence, level) // text fast-path
|
||||
}
|
||||
|
||||
now := l.timeNow()
|
||||
@@ -544,10 +605,18 @@ func (l *Logger) encode(buf []byte, level int) []byte {
|
||||
obj["error_has_logtail"] = obj["logtail"]
|
||||
obj["logtail"] = nil
|
||||
}
|
||||
if !l.skipClientTime {
|
||||
obj["logtail"] = map[string]string{
|
||||
"client_time": now.Format(time.RFC3339Nano),
|
||||
if !l.skipClientTime || l.procID != 0 || l.procSequence != 0 {
|
||||
logtail := map[string]any{}
|
||||
if !l.skipClientTime {
|
||||
logtail["client_time"] = now.Format(time.RFC3339Nano)
|
||||
}
|
||||
if l.procID != 0 {
|
||||
logtail["proc_id"] = l.procID
|
||||
}
|
||||
if l.procSequence != 0 {
|
||||
logtail["proc_seq"] = l.procSequence
|
||||
}
|
||||
obj["logtail"] = logtail
|
||||
}
|
||||
if level > 0 {
|
||||
obj["v"] = level
|
||||
@@ -590,8 +659,10 @@ func (l *Logger) Write(buf []byte) (int, error) {
|
||||
l.stderr.Write(withNL)
|
||||
}
|
||||
}
|
||||
l.writeLock.Lock()
|
||||
b := l.encode(buf, level)
|
||||
_, err := l.send(b)
|
||||
l.writeLock.Unlock()
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
|
||||
@@ -216,8 +216,10 @@ var sink []byte
|
||||
func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now}
|
||||
inBuf := []byte("some text to encode")
|
||||
procID := uint32(0x24d32ee9)
|
||||
procSequence := uint64(0x12346)
|
||||
err := tstest.MinAllocsPerRun(t, 1, func() {
|
||||
sink = lg.encodeText(inBuf, false, 0)
|
||||
sink = lg.encodeText(inBuf, false, procID, procSequence, 0)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -333,7 +335,7 @@ func unmarshalOne(t *testing.T, body []byte) map[string]any {
|
||||
func TestEncodeTextTruncation(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now, lowMem: true}
|
||||
in := bytes.Repeat([]byte("a"), 300)
|
||||
b := lg.encodeText(in, true, 0)
|
||||
b := lg.encodeText(in, true, 0, 0, 0)
|
||||
got := string(b)
|
||||
want := `{"text": "` + strings.Repeat("a", 255) + `…+45"}` + "\n"
|
||||
if got != want {
|
||||
@@ -355,38 +357,40 @@ func TestEncode(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"normal",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z"}, "text": "normal"}` + "\n",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "text": "normal"}` + "\n",
|
||||
},
|
||||
{
|
||||
"and a [v1] level one",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z"}, "v":1,"text": "and a level one"}` + "\n",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "v":1,"text": "and a level one"}` + "\n",
|
||||
},
|
||||
{
|
||||
"[v2] some verbose two",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z"}, "v":2,"text": "some verbose two"}` + "\n",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "v":2,"text": "some verbose two"}` + "\n",
|
||||
},
|
||||
{
|
||||
"{}",
|
||||
`{"logtail":{"client_time":"1970-01-01T00:02:03.000000456Z"}}` + "\n",
|
||||
`{"logtail":{"client_time":"1970-01-01T00:02:03.000000456Z","proc_id":7,"proc_seq":1}}` + "\n",
|
||||
},
|
||||
{
|
||||
`{"foo":"bar"}`,
|
||||
`{"foo":"bar","logtail":{"client_time":"1970-01-01T00:02:03.000000456Z"}}` + "\n",
|
||||
`{"foo":"bar","logtail":{"client_time":"1970-01-01T00:02:03.000000456Z","proc_id":7,"proc_seq":1}}` + "\n",
|
||||
},
|
||||
{
|
||||
"foo: [v\x00JSON]0{\"foo\":1}",
|
||||
"{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\"}}\n",
|
||||
"{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\",\"proc_id\":7,\"proc_seq\":1}}\n",
|
||||
},
|
||||
{
|
||||
"foo: [v\x00JSON]2{\"foo\":1}",
|
||||
"{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\"},\"v\":2}\n",
|
||||
"{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\",\"proc_id\":7,\"proc_seq\":1},\"v\":2}\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
buf := new(simpleMemBuf)
|
||||
lg := &Logger{
|
||||
timeNow: func() time.Time { return time.Unix(123, 456).UTC() },
|
||||
buffer: buf,
|
||||
timeNow: func() time.Time { return time.Unix(123, 456).UTC() },
|
||||
buffer: buf,
|
||||
procID: 7,
|
||||
procSequence: 1,
|
||||
}
|
||||
io.WriteString(lg, tt.in)
|
||||
got := buf.buf.String()
|
||||
|
||||
@@ -22,14 +22,14 @@ type Config struct {
|
||||
// which aren't covered by more specific per-domain routes below.
|
||||
// If empty, the OS's default resolvers (the ones that predate
|
||||
// Tailscale altering the configuration) are used.
|
||||
DefaultResolvers []dnstype.Resolver
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
// Routes maps a DNS suffix to the resolvers that should be used
|
||||
// for queries that fall within that suffix.
|
||||
// If a query doesn't match any entry in Routes, the
|
||||
// DefaultResolvers are used.
|
||||
// A Routes entry with no resolvers means the route should be
|
||||
// authoritatively answered using the contents of Hosts.
|
||||
Routes map[dnsname.FQDN][]dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
// SearchDomains are DNS suffixes to try when expanding
|
||||
// single-label queries.
|
||||
SearchDomains []dnsname.FQDN
|
||||
@@ -98,9 +98,9 @@ func (c Config) hasDefaultResolvers() bool {
|
||||
// singleResolverSet returns the resolvers used by c.Routes if all
|
||||
// routes use the same resolvers, or nil if multiple sets of resolvers
|
||||
// are specified.
|
||||
func (c Config) singleResolverSet() []dnstype.Resolver {
|
||||
func (c Config) singleResolverSet() []*dnstype.Resolver {
|
||||
var (
|
||||
prev []dnstype.Resolver
|
||||
prev []*dnstype.Resolver
|
||||
prevInitialized bool
|
||||
)
|
||||
for _, resolvers := range c.Routes {
|
||||
@@ -128,7 +128,7 @@ func (c Config) matchDomains() []dnsname.FQDN {
|
||||
return ret
|
||||
}
|
||||
|
||||
func sameResolverNames(a, b []dnstype.Resolver) bool {
|
||||
func sameResolverNames(a, b []*dnstype.Resolver) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
// authoritative suffixes, even if we don't propagate MagicDNS to
|
||||
// the OS.
|
||||
rcfg.Hosts = cfg.Hosts
|
||||
routes := map[dnsname.FQDN][]dnstype.Resolver{} // assigned conditionally to rcfg.Routes below.
|
||||
routes := map[dnsname.FQDN][]*dnstype.Resolver{} // assigned conditionally to rcfg.Routes below.
|
||||
for suffix, resolvers := range cfg.Routes {
|
||||
if len(resolvers) == 0 {
|
||||
rcfg.LocalDomains = append(rcfg.LocalDomains, suffix)
|
||||
@@ -225,9 +225,9 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
health.SetDNSOSHealth(err)
|
||||
return resolver.Config{}, OSConfig{}, err
|
||||
}
|
||||
var defaultRoutes []dnstype.Resolver
|
||||
var defaultRoutes []*dnstype.Resolver
|
||||
for _, ip := range bcfg.Nameservers {
|
||||
defaultRoutes = append(defaultRoutes, dnstype.Resolver{Addr: ip.String()})
|
||||
defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
|
||||
}
|
||||
rcfg.Routes["."] = defaultRoutes
|
||||
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
|
||||
@@ -239,7 +239,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
// toIPsOnly returns only the IP portion of dnstype.Resolver.
|
||||
// Only safe to use if the resolvers slice has been cleared of
|
||||
// DoH or custom-port entries with something like hasDefaultIPResolversOnly.
|
||||
func toIPsOnly(resolvers []dnstype.Resolver) (ret []netaddr.IP) {
|
||||
func toIPsOnly(resolvers []*dnstype.Resolver) (ret []netaddr.IP) {
|
||||
for _, r := range resolvers {
|
||||
if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
|
||||
ret = append(ret, ipp.IP())
|
||||
@@ -331,6 +331,9 @@ func (m *Manager) NextPacket() ([]byte, error) {
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Query executes a DNS query recieved from the given address. The query is
|
||||
// provided in bs as a wire-encoded DNS query without any transport header.
|
||||
// This method is called for requests arriving over UDP and TCP.
|
||||
func (m *Manager) Query(ctx context.Context, bs []byte, from netaddr.IPPort) ([]byte, error) {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
@@ -460,7 +463,7 @@ func (m *Manager) HandleTCPConn(conn net.Conn, srcAddr netaddr.IPPort) {
|
||||
responses: make(chan []byte),
|
||||
readClosing: make(chan struct{}),
|
||||
}
|
||||
s.ctx, s.closeCtx = context.WithCancel(context.Background())
|
||||
s.ctx, s.closeCtx = context.WithCancel(m.ctx)
|
||||
go s.handleReads()
|
||||
s.handleWrites()
|
||||
}
|
||||
|
||||
@@ -437,9 +437,9 @@ func mustIPPs(strs ...string) (ret []netaddr.IPPort) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func mustRes(strs ...string) (ret []dnstype.Resolver) {
|
||||
func mustRes(strs ...string) (ret []*dnstype.Resolver) {
|
||||
for _, s := range strs {
|
||||
ret = append(ret, dnstype.Resolver{Addr: s})
|
||||
ret = append(ret, &dnstype.Resolver{Addr: s})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -495,9 +495,9 @@ func hostsR(strs ...string) (ret map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func upstreams(strs ...string) (ret map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
func upstreams(strs ...string) (ret map[dnsname.FQDN][]*dnstype.Resolver) {
|
||||
var key dnsname.FQDN
|
||||
ret = map[dnsname.FQDN][]dnstype.Resolver{}
|
||||
ret = map[dnsname.FQDN][]*dnstype.Resolver{}
|
||||
for _, s := range strs {
|
||||
if s == "" {
|
||||
if key == "" {
|
||||
@@ -508,14 +508,14 @@ func upstreams(strs ...string) (ret map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
if key == "" {
|
||||
panic("IPPort provided before suffix")
|
||||
}
|
||||
ret[key] = append(ret[key], dnstype.Resolver{Addr: ipp.String()})
|
||||
ret[key] = append(ret[key], &dnstype.Resolver{Addr: ipp.String()})
|
||||
} else if _, err := netaddr.ParseIP(s); err == nil {
|
||||
if key == "" {
|
||||
panic("IPPort provided before suffix")
|
||||
}
|
||||
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
|
||||
ret[key] = append(ret[key], &dnstype.Resolver{Addr: s})
|
||||
} else if strings.HasPrefix(s, "http") {
|
||||
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
|
||||
ret[key] = append(ret[key], &dnstype.Resolver{Addr: s})
|
||||
} else {
|
||||
fqdn, err := dnsname.ToFQDN(s)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,18 +20,27 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
||||
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
|
||||
|
||||
// the GUID is randomly generated. At present, Tailscale installs
|
||||
// zero or one NRPT rules, so hardcoding a single GUID everywhere
|
||||
// is fine.
|
||||
nrptBase = `SYSTEM\CurrentControlSet\services\Dnscache\Parameters\DnsPolicyConfig\{5abe529b-675b-4486-8459-25a634dacc23}`
|
||||
nrptBase = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\`
|
||||
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
||||
|
||||
// This is the legacy rule ID that previous versions used when we supported
|
||||
// only a single rule. Now that we support multiple rules are required, we
|
||||
// generate their GUIDs and store them under the Tailscale registry key.
|
||||
nrptSingleRuleID = `{5abe529b-675b-4486-8459-25a634dacc23}`
|
||||
// Apparently NRPT rules cannot handle > 50 domains.
|
||||
nrptMaxDomainsPerRule = 50
|
||||
|
||||
// This is the name of the registry value we use to save Rule IDs under
|
||||
// the Tailscale registry key.
|
||||
nrptRuleIDValueName = `NRPTRuleIDs`
|
||||
|
||||
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
|
||||
)
|
||||
|
||||
@@ -44,6 +53,15 @@ type windowsManager struct {
|
||||
wslManager *wslManager
|
||||
}
|
||||
|
||||
func loadRuleSubkeyNames() []string {
|
||||
result := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if result == nil {
|
||||
// Use the legacy rule ID if none are specified in our registry key
|
||||
result = []string{nrptSingleRuleID}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
ret := windowsManager{
|
||||
logf: logf,
|
||||
@@ -59,7 +77,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator,
|
||||
// boot up. The bootstrap resolver logic will save us, but it
|
||||
// slows down start-up a bunch.
|
||||
if ret.nrptWorks {
|
||||
ret.delKey(nrptBase)
|
||||
ret.delAllRuleKeys()
|
||||
}
|
||||
|
||||
// Log WSL status once at startup.
|
||||
@@ -90,10 +108,26 @@ func (m windowsManager) ifPath(basePath string) string {
|
||||
return fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
}
|
||||
|
||||
func (m windowsManager) delKey(path string) error {
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, path); err != nil && err != registry.ErrNotExist {
|
||||
func (m windowsManager) delAllRuleKeys() error {
|
||||
nrptRuleIDs := loadRuleSubkeyNames()
|
||||
if err := m.delRuleKeys(nrptRuleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
m.logf("Error deleting registry value %q: %v", nrptRuleIDValueName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) delRuleKeys(nrptRuleIDs []string) error {
|
||||
for _, rid := range nrptRuleIDs {
|
||||
keyName := nrptBase + rid
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyName); err != nil && err != registry.ErrNotExist {
|
||||
m.logf("Error deleting NRPT rule key %q: %v", keyName, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -104,31 +138,91 @@ func delValue(key registry.Key, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSplitDNS configures an NRPT (Name Resolution Policy Table) rule
|
||||
// setSplitDNS configures one or more NRPT (Name Resolution Policy Table) rules
|
||||
// to resolve queries for domains using resolvers, rather than the
|
||||
// system's "primary" resolver.
|
||||
//
|
||||
// If no resolvers are provided, the Tailscale NRPT rule is deleted.
|
||||
// If no resolvers are provided, the Tailscale NRPT rules are deleted.
|
||||
func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
|
||||
if len(resolvers) == 0 {
|
||||
return m.delKey(nrptBase)
|
||||
return m.delAllRuleKeys()
|
||||
}
|
||||
|
||||
servers := make([]string, 0, len(resolvers))
|
||||
for _, resolver := range resolvers {
|
||||
servers = append(servers, resolver.String())
|
||||
}
|
||||
doms := make([]string, 0, len(domains))
|
||||
for _, domain := range domains {
|
||||
// NRPT rules must have a leading dot, which is not usual for
|
||||
// DNS search paths.
|
||||
doms = append(doms, "."+domain.WithoutTrailingDot())
|
||||
|
||||
// NRPT has an undocumented restriction that each rule may only be associated
|
||||
// with a maximum of 50 domains. If we are setting rules for more domains
|
||||
// than that, we need to split domains into chunks and write out a rule per chunk.
|
||||
dq := len(domains) / nrptMaxDomainsPerRule
|
||||
dr := len(domains) % nrptMaxDomainsPerRule
|
||||
|
||||
domainRulesLen := dq
|
||||
if dr > 0 {
|
||||
domainRulesLen++
|
||||
}
|
||||
|
||||
nrptRuleIDs := loadRuleSubkeyNames()
|
||||
for len(nrptRuleIDs) < domainRulesLen {
|
||||
guid, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nrptRuleIDs = append(nrptRuleIDs, guid.String())
|
||||
}
|
||||
|
||||
// Remove any surplus rules that are no longer needed.
|
||||
ruleIDsToRemove := nrptRuleIDs[domainRulesLen:]
|
||||
m.delRuleKeys(ruleIDsToRemove)
|
||||
|
||||
// We need to save the list of rule IDs to our Tailscale registry key so that
|
||||
// we know which rules are ours during subsequent modifications to NRPT rules.
|
||||
ruleIDsToWrite := nrptRuleIDs[:domainRulesLen]
|
||||
if len(ruleIDsToWrite) > 0 {
|
||||
if err := winutil.SetRegStrings(nrptRuleIDValueName, ruleIDsToWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
doms := make([]string, 0, nrptMaxDomainsPerRule)
|
||||
for i := 0; i < domainRulesLen; i++ {
|
||||
// Each iteration consumes nrptMaxDomainsPerRule domains...
|
||||
curLen := nrptMaxDomainsPerRule
|
||||
// Except for the final iteration: when we have a remainder, use that instead.
|
||||
if i == domainRulesLen-1 && dr > 0 {
|
||||
curLen = dr
|
||||
}
|
||||
|
||||
// Obtain the slice of domains to consume within the current iteration.
|
||||
start := i * nrptMaxDomainsPerRule
|
||||
end := start + curLen
|
||||
for _, domain := range domains[start:end] {
|
||||
// NRPT rules must have a leading dot, which is not usual for
|
||||
// DNS search paths.
|
||||
doms = append(doms, "."+domain.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if err := writeNRPTRule(nrptRuleIDs[i], doms, servers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doms = doms[:0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeNRPTRule(ruleID string, doms, servers []string) error {
|
||||
// CreateKey is actually open-or-create, which suits us fine.
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase, registry.SET_VALUE)
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase+ruleID, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", nrptBase, err)
|
||||
return fmt.Errorf("opening %s: %w", nrptBase+ruleID, err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.SetDWordValue("Version", 1); err != nil {
|
||||
|
||||
160
net/dns/manager_windows_test.go
Normal file
160
net/dns/manager_windows_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
func TestManagerWindows(t *testing.T) {
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
t.Skipf("test requires running as elevated user")
|
||||
}
|
||||
|
||||
logf := func(format string, args ...any) {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
|
||||
fakeInterface, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
t.Fatalf("windows.GenerateGUID: %v\n", err)
|
||||
}
|
||||
|
||||
cfg, err := NewOSConfigurator(logf, fakeInterface.String())
|
||||
if err != nil {
|
||||
t.Fatalf("NewOSConfigurator: %v\n", err)
|
||||
}
|
||||
mgr := cfg.(windowsManager)
|
||||
|
||||
// Upon initialization of cfg, we should not have any NRPT rules
|
||||
ensureNoRules(t)
|
||||
|
||||
resolvers := []netaddr.IP{netaddr.MustParseIP("1.1.1.1")}
|
||||
|
||||
domains := make([]dnsname.FQDN, 0, 2*nrptMaxDomainsPerRule+1)
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// Just generate a bunch of random subdomains
|
||||
for len(domains) < cap(domains) {
|
||||
l := r.Intn(19) + 1
|
||||
b := make([]byte, l)
|
||||
for i, _ := range b {
|
||||
b[i] = charset[r.Intn(len(charset))]
|
||||
}
|
||||
d := string(b) + ".example.com"
|
||||
fqdn, err := dnsname.ToFQDN(d)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsname.ToFQDN: %v\n", err)
|
||||
}
|
||||
domains = append(domains, fqdn)
|
||||
}
|
||||
|
||||
cases := []int{
|
||||
1,
|
||||
50,
|
||||
51,
|
||||
100,
|
||||
101,
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
51,
|
||||
}
|
||||
|
||||
for _, n := range cases {
|
||||
t.Logf("Test case: %d domains\n", n)
|
||||
caseDomains := domains[:n]
|
||||
err := mgr.setSplitDNS(resolvers, caseDomains)
|
||||
if err != nil {
|
||||
t.Fatalf("setSplitDNS: %v\n", err)
|
||||
}
|
||||
validateRegistry(t, caseDomains)
|
||||
}
|
||||
|
||||
t.Logf("Test case: nil resolver\n")
|
||||
err = mgr.setSplitDNS(nil, domains)
|
||||
if err != nil {
|
||||
t.Fatalf("setSplitDNS: %v\n", err)
|
||||
}
|
||||
ensureNoRules(t)
|
||||
}
|
||||
|
||||
func ensureNoRules(t *testing.T) {
|
||||
ruleIDs := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if ruleIDs != nil {
|
||||
t.Errorf("%s: %v, want nil\n", nrptRuleIDValueName, ruleIDs)
|
||||
}
|
||||
|
||||
legacyKeyPath := nrptBase + nrptSingleRuleID
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, legacyKeyPath, registry.READ)
|
||||
if err == nil {
|
||||
key.Close()
|
||||
}
|
||||
if err != registry.ErrNotExist {
|
||||
t.Errorf("%s: %q, want %q\n", legacyKeyPath, err, registry.ErrNotExist)
|
||||
}
|
||||
}
|
||||
|
||||
func validateRegistry(t *testing.T, domains []dnsname.FQDN) {
|
||||
q := len(domains) / nrptMaxDomainsPerRule
|
||||
r := len(domains) % nrptMaxDomainsPerRule
|
||||
numRules := q
|
||||
if r > 0 {
|
||||
numRules++
|
||||
}
|
||||
|
||||
ruleIDs := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if ruleIDs == nil {
|
||||
ruleIDs = []string{nrptSingleRuleID}
|
||||
} else if len(ruleIDs) != numRules {
|
||||
t.Errorf("%s for %d domains: %d, want %d\n", nrptRuleIDValueName, len(domains), len(ruleIDs), numRules)
|
||||
}
|
||||
|
||||
for i, ruleID := range ruleIDs {
|
||||
savedDomains, err := getSavedDomainsForRule(ruleID)
|
||||
if err != nil {
|
||||
t.Fatalf("getSavedDomainsForRule(%q): %v\n", ruleID, err)
|
||||
}
|
||||
|
||||
start := i * nrptMaxDomainsPerRule
|
||||
end := start + nrptMaxDomainsPerRule
|
||||
if i == len(ruleIDs)-1 && r > 0 {
|
||||
end = start + r
|
||||
}
|
||||
|
||||
checkDomains := domains[start:end]
|
||||
if len(checkDomains) != len(savedDomains) {
|
||||
t.Errorf("len(checkDomains) != len(savedDomains): %d, want %d\n", len(savedDomains), len(checkDomains))
|
||||
}
|
||||
for j, cd := range checkDomains {
|
||||
sd := strings.TrimPrefix(savedDomains[j], ".")
|
||||
if string(cd.WithoutTrailingDot()) != sd {
|
||||
t.Errorf("checkDomain differs savedDomain: %s, want %s\n", sd, cd.WithoutTrailingDot())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSavedDomainsForRule(ruleID string) ([]string, error) {
|
||||
keyPath := nrptBase + ruleID
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.READ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer key.Close()
|
||||
result, _, err := key.GetStringsValue("Name")
|
||||
return result, err
|
||||
}
|
||||
@@ -170,7 +170,7 @@ type route struct {
|
||||
// long to wait before querying it.
|
||||
type resolverAndDelay struct {
|
||||
// name is the upstream resolver.
|
||||
name dnstype.Resolver
|
||||
name *dnstype.Resolver
|
||||
|
||||
// startDelay is an amount to delay this resolver at
|
||||
// start. It's used when, say, there are four Google or
|
||||
@@ -246,7 +246,7 @@ func (f *forwarder) Close() error {
|
||||
// resolversWithDelays maps from a set of DNS server names to a slice of a type
|
||||
// that included a startDelay, upgrading any well-known DoH (DNS-over-HTTP)
|
||||
// servers in the process, insert a DoH lookup first before UDP fallbacks.
|
||||
func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
|
||||
func resolversWithDelays(resolvers []*dnstype.Resolver) []resolverAndDelay {
|
||||
rr := make([]resolverAndDelay, 0, len(resolvers)+2)
|
||||
|
||||
// Add the known DoH ones first, starting immediately.
|
||||
@@ -261,7 +261,7 @@ func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
|
||||
continue
|
||||
}
|
||||
didDoH[dohBase] = true
|
||||
rr = append(rr, resolverAndDelay{name: dnstype.Resolver{Addr: dohBase}})
|
||||
rr = append(rr, resolverAndDelay{name: &dnstype.Resolver{Addr: dohBase}})
|
||||
}
|
||||
|
||||
type hostAndFam struct {
|
||||
@@ -300,7 +300,7 @@ func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
|
||||
// Resolver.SetConfig on reconfig.
|
||||
//
|
||||
// The memory referenced by routesBySuffix should not be modified.
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolver) {
|
||||
routes := make([]route, 0, len(routesBySuffix))
|
||||
for suffix, rs := range routesBySuffix {
|
||||
routes = append(routes, route{
|
||||
|
||||
@@ -23,9 +23,9 @@ func (rr resolverAndDelay) String() string {
|
||||
|
||||
func TestResolversWithDelays(t *testing.T) {
|
||||
// query
|
||||
q := func(ss ...string) (ipps []dnstype.Resolver) {
|
||||
q := func(ss ...string) (ipps []*dnstype.Resolver) {
|
||||
for _, host := range ss {
|
||||
ipps = append(ipps, dnstype.Resolver{Addr: host})
|
||||
ipps = append(ipps, &dnstype.Resolver{Addr: host})
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func TestResolversWithDelays(t *testing.T) {
|
||||
}
|
||||
}
|
||||
rr = append(rr, resolverAndDelay{
|
||||
name: dnstype.Resolver{Addr: s},
|
||||
name: &dnstype.Resolver{Addr: s},
|
||||
startDelay: d,
|
||||
})
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func TestResolversWithDelays(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in []dnstype.Resolver
|
||||
in []*dnstype.Resolver
|
||||
want []resolverAndDelay
|
||||
}{
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ type Config struct {
|
||||
// queries within that suffix.
|
||||
// Queries only match the most specific suffix.
|
||||
// To register a "default route", add an entry for ".".
|
||||
Routes map[dnsname.FQDN][]dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
// LocalHosts is a map of FQDNs to corresponding IPs.
|
||||
Hosts map[dnsname.FQDN][]netaddr.IP
|
||||
// LocalDomains is a list of DNS name suffixes that should not be
|
||||
@@ -115,7 +115,7 @@ func WriteIPPorts(w *bufio.Writer, vv []netaddr.IPPort) {
|
||||
}
|
||||
|
||||
// WriteDNSResolver writes r to w.
|
||||
func WriteDNSResolver(w *bufio.Writer, r dnstype.Resolver) {
|
||||
func WriteDNSResolver(w *bufio.Writer, r *dnstype.Resolver) {
|
||||
io.WriteString(w, r.Addr)
|
||||
if len(r.BootstrapResolution) > 0 {
|
||||
w.WriteByte('(')
|
||||
@@ -129,7 +129,7 @@ func WriteDNSResolver(w *bufio.Writer, r dnstype.Resolver) {
|
||||
}
|
||||
|
||||
// WriteDNSResolvers writes resolvers to w.
|
||||
func WriteDNSResolvers(w *bufio.Writer, resolvers []dnstype.Resolver) {
|
||||
func WriteDNSResolvers(w *bufio.Writer, resolvers []*dnstype.Resolver) {
|
||||
w.WriteByte('[')
|
||||
for i, r := range resolvers {
|
||||
if i > 0 {
|
||||
@@ -142,7 +142,7 @@ func WriteDNSResolvers(w *bufio.Writer, resolvers []dnstype.Resolver) {
|
||||
|
||||
// WriteRoutes writes routes to w, omitting *.arpa routes and instead
|
||||
// summarizing how many of them there were.
|
||||
func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]*dnstype.Resolver) {
|
||||
var kk []dnsname.FQDN
|
||||
arpa := 0
|
||||
for k := range routes {
|
||||
@@ -256,6 +256,11 @@ func (r *Resolver) Close() {
|
||||
r.forwarder.Close()
|
||||
}
|
||||
|
||||
// dnsQueryTimeout is not intended to be user-visible (the users
|
||||
// DNS resolver will retry well before that), just put an upper
|
||||
// bound on per-query resource usage.
|
||||
const dnsQueryTimeout = 10 * time.Second
|
||||
|
||||
func (r *Resolver) Query(ctx context.Context, bs []byte, from netaddr.IPPort) ([]byte, error) {
|
||||
metricDNSQueryLocal.Add(1)
|
||||
select {
|
||||
@@ -268,7 +273,7 @@ func (r *Resolver) Query(ctx context.Context, bs []byte, from netaddr.IPPort) ([
|
||||
out, err := r.respond(bs)
|
||||
if err == errNotOurName {
|
||||
responses := make(chan packet, 1)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
ctx, cancel := context.WithTimeout(ctx, dnsQueryTimeout)
|
||||
defer close(responses)
|
||||
defer cancel()
|
||||
err = r.forwarder.forwardWithDestChan(ctx, packet{bs, from}, responses)
|
||||
@@ -347,7 +352,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
|
||||
// will use its default ones from our DNS config.
|
||||
} else {
|
||||
resolvers = []resolverAndDelay{{
|
||||
name: dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
|
||||
name: &dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -611,11 +616,16 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
|
||||
}
|
||||
}
|
||||
|
||||
// parseViaDomain synthesizes an IP address for quad-A DNS requests of
|
||||
// the form 'via-<X>.<IPv4-address>', where X is a decimal, or hex-encoded
|
||||
// number with a '0x' prefix.
|
||||
// parseViaDomain synthesizes an IP address for quad-A DNS requests of the form
|
||||
// `<IPv4-address>.via-<X>` and the deprecated form `via-<X>.<IPv4-address>`,
|
||||
// where X is a decimal, or hex-encoded number with a '0x' prefix.
|
||||
//
|
||||
// This exists as a convenient mapping into Tailscales 'Via Range'.
|
||||
//
|
||||
// TODO(maisem/bradfitz/tom): `<IPv4-address>.via-<X>` was introduced
|
||||
// (2022-06-02) to work around an issue in Chrome where it would treat
|
||||
// "http://via-1.1.2.3.4" as a search string instead of a URL. We should rip out
|
||||
// the old format in early 2023.
|
||||
func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netaddr.IP, bool) {
|
||||
fqdn := string(domain.WithoutTrailingDot())
|
||||
if typ != dns.TypeAAAA {
|
||||
@@ -624,18 +634,29 @@ func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netaddr.IP
|
||||
if len(fqdn) < len("via-X.0.0.0.0") {
|
||||
return netaddr.IP{}, false // too short to be valid
|
||||
}
|
||||
if !strings.HasPrefix(fqdn, "via-") {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
|
||||
firstDot := strings.Index(fqdn, ".")
|
||||
if firstDot < 0 {
|
||||
return netaddr.IP{}, false // missing dot delimiters
|
||||
var siteID string
|
||||
var ip4Str string
|
||||
if strings.HasPrefix(fqdn, "via-") {
|
||||
firstDot := strings.Index(fqdn, ".")
|
||||
if firstDot < 0 {
|
||||
return netaddr.IP{}, false // missing dot delimiters
|
||||
}
|
||||
siteID = fqdn[len("via-"):firstDot]
|
||||
ip4Str = fqdn[firstDot+1:]
|
||||
} else {
|
||||
lastDot := strings.LastIndex(fqdn, ".")
|
||||
if lastDot < 0 {
|
||||
return netaddr.IP{}, false // missing dot delimiters
|
||||
}
|
||||
suffix := fqdn[lastDot+1:]
|
||||
if !strings.HasPrefix(suffix, "via-") {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
siteID = suffix[len("via-"):]
|
||||
ip4Str = fqdn[:lastDot]
|
||||
}
|
||||
|
||||
siteID := fqdn[len("via-"):firstDot]
|
||||
ip4Str := fqdn[firstDot+1:]
|
||||
|
||||
ip4, err := netaddr.ParseIP(ip4Str)
|
||||
if err != nil {
|
||||
return netaddr.IP{}, false // badly formed, dont respond
|
||||
|
||||
@@ -343,9 +343,12 @@ func TestResolveLocal(t *testing.T) {
|
||||
{"ns-nxdomain", "test3.ipn.dev.", dns.TypeNS, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"onion-domain", "footest.onion.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"magicdns", dnsSymbolicFQDN, dns.TypeA, netaddr.MustParseIP("100.100.100.100"), dns.RCodeSuccess},
|
||||
{"via_hex", dnsname.FQDN("via-0xff.1.2.3.4."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:ff:102:304"), dns.RCodeSuccess},
|
||||
{"via_dec", dnsname.FQDN("via-1.10.0.0.1."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:1:a00:1"), dns.RCodeSuccess},
|
||||
{"via_invalid", dnsname.FQDN("via-."), dns.TypeA, netaddr.IP{}, dns.RCodeRefused},
|
||||
{"via_hex", dnsname.FQDN("via-0xff.1.2.3.4."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:ff:1.2.3.4"), dns.RCodeSuccess},
|
||||
{"via_dec", dnsname.FQDN("via-1.10.0.0.1."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:1:10.0.0.1"), dns.RCodeSuccess},
|
||||
{"x_via_hex", dnsname.FQDN("4.3.2.1.via-0xff."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:ff:4.3.2.1"), dns.RCodeSuccess},
|
||||
{"x_via_dec", dnsname.FQDN("1.0.0.10.via-1."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:1:1.0.0.10"), dns.RCodeSuccess},
|
||||
{"via_invalid", dnsname.FQDN("via-."), dns.TypeAAAA, netaddr.IP{}, dns.RCodeRefused},
|
||||
{"via_invalid_2", dnsname.FQDN("2.3.4.5.via-."), dns.TypeAAAA, netaddr.IP{}, dns.RCodeRefused},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -480,10 +483,10 @@ func TestDelegate(t *testing.T) {
|
||||
defer r.Close()
|
||||
|
||||
cfg := dnsCfg
|
||||
cfg.Routes = map[dnsname.FQDN][]dnstype.Resolver{
|
||||
cfg.Routes = map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
".": {
|
||||
dnstype.Resolver{Addr: v4server.PacketConn.LocalAddr().String()},
|
||||
dnstype.Resolver{Addr: v6server.PacketConn.LocalAddr().String()},
|
||||
&dnstype.Resolver{Addr: v4server.PacketConn.LocalAddr().String()},
|
||||
&dnstype.Resolver{Addr: v6server.PacketConn.LocalAddr().String()},
|
||||
},
|
||||
}
|
||||
r.SetConfig(cfg)
|
||||
@@ -655,7 +658,7 @@ func TestDelegateSplitRoute(t *testing.T) {
|
||||
defer r.Close()
|
||||
|
||||
cfg := dnsCfg
|
||||
cfg.Routes = map[dnsname.FQDN][]dnstype.Resolver{
|
||||
cfg.Routes = map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
".": {{Addr: server1.PacketConn.LocalAddr().String()}},
|
||||
"other.": {{Addr: server2.PacketConn.LocalAddr().String()}},
|
||||
}
|
||||
@@ -947,7 +950,7 @@ func BenchmarkFull(b *testing.B) {
|
||||
defer r.Close()
|
||||
|
||||
cfg := dnsCfg
|
||||
cfg.Routes = map[dnsname.FQDN][]dnstype.Resolver{
|
||||
cfg.Routes = map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
".": {{Addr: server.PacketConn.LocalAddr().String()}},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios
|
||||
// +build !ios
|
||||
//go:build !ios && !js
|
||||
// +build !ios,!js
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ios
|
||||
// +build ios
|
||||
//go:build ios || js
|
||||
// +build ios js
|
||||
|
||||
// (https://github.com/tailscale/tailscale/issues/2495)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios
|
||||
// +build !ios
|
||||
//go:build !ios && !js
|
||||
// +build !ios,!js
|
||||
|
||||
// (https://github.com/tailscale/tailscale/issues/2495)
|
||||
|
||||
|
||||
16
net/tstun/constants.go
Normal file
16
net/tstun/constants.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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 tstun
|
||||
|
||||
// DefaultMTU is the Tailscale default MTU for now.
|
||||
//
|
||||
// wireguard-go defaults to 1420 bytes, which only works if the
|
||||
// "outer" MTU is 1500 bytes. This breaks on DSL connections
|
||||
// (typically 1492 MTU) and on GCE (1460 MTU?!).
|
||||
//
|
||||
// 1280 is the smallest MTU allowed for IPv6, which is a sensible
|
||||
// "probably works everywhere" setting until we develop proper PMTU
|
||||
// discovery.
|
||||
const DefaultMTU = 1280
|
||||
@@ -20,15 +20,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// tunMTU is the MTU we set on tailscale's TUN interface. wireguard-go
|
||||
// defaults to 1420 bytes, which only works if the "outer" MTU is 1500
|
||||
// bytes. This breaks on DSL connections (typically 1492 MTU) and on
|
||||
// GCE (1460 MTU?!).
|
||||
//
|
||||
// 1280 is the smallest MTU allowed for IPv6, which is a sensible
|
||||
// "probably works everywhere" setting until we develop proper PMTU
|
||||
// discovery.
|
||||
var tunMTU = 1280
|
||||
var tunMTU = DefaultMTU
|
||||
|
||||
func init() {
|
||||
if mtu, ok := envknob.LookupInt("TS_DEBUG_MTU"); ok {
|
||||
|
||||
@@ -831,13 +831,13 @@ func (t *Wrapper) Unwrap() tun.Device {
|
||||
}
|
||||
|
||||
var (
|
||||
metricPacketIn = clientmetric.NewGauge("tstun_in_from_wg")
|
||||
metricPacketInDrop = clientmetric.NewGauge("tstun_in_from_wg_drop")
|
||||
metricPacketInDropFilter = clientmetric.NewGauge("tstun_in_from_wg_drop_filter")
|
||||
metricPacketInDropSelfDisco = clientmetric.NewGauge("tstun_in_from_wg_drop_self_disco")
|
||||
metricPacketIn = clientmetric.NewCounter("tstun_in_from_wg")
|
||||
metricPacketInDrop = clientmetric.NewCounter("tstun_in_from_wg_drop")
|
||||
metricPacketInDropFilter = clientmetric.NewCounter("tstun_in_from_wg_drop_filter")
|
||||
metricPacketInDropSelfDisco = clientmetric.NewCounter("tstun_in_from_wg_drop_self_disco")
|
||||
|
||||
metricPacketOut = clientmetric.NewGauge("tstun_out_to_wg")
|
||||
metricPacketOutDrop = clientmetric.NewGauge("tstun_out_to_wg_drop")
|
||||
metricPacketOutDropFilter = clientmetric.NewGauge("tstun_out_to_wg_drop_filter")
|
||||
metricPacketOutDropSelfDisco = clientmetric.NewGauge("tstun_out_to_wg_drop_self_disco")
|
||||
metricPacketOut = clientmetric.NewCounter("tstun_out_to_wg")
|
||||
metricPacketOutDrop = clientmetric.NewCounter("tstun_out_to_wg_drop")
|
||||
metricPacketOutDropFilter = clientmetric.NewCounter("tstun_out_to_wg_drop_filter")
|
||||
metricPacketOutDropSelfDisco = clientmetric.NewCounter("tstun_out_to_wg_drop_self_disco")
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ func New(c *websocket.Conn) net.Conn {
|
||||
return &websocketConn{c: c}
|
||||
}
|
||||
|
||||
// websocketConn implements derp.Conn around a *websocket.Conn,
|
||||
// websocketConn implements net.Conn around a *websocket.Conn,
|
||||
// treating a websocket.Conn as a byte stream, ignoring the WebSocket
|
||||
// frame/message boundaries.
|
||||
type websocketConn struct {
|
||||
@@ -8,67 +8,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
func getTokenInfo(token windows.Token, infoClass uint32) ([]byte, error) {
|
||||
var desiredLen uint32
|
||||
err := windows.GetTokenInformation(token, infoClass, nil, 0, &desiredLen)
|
||||
if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := make([]byte, desiredLen)
|
||||
actualLen := desiredLen
|
||||
err = windows.GetTokenInformation(token, infoClass, &buf[0], desiredLen, &actualLen)
|
||||
return buf, err
|
||||
}
|
||||
|
||||
func getTokenUserInfo(token windows.Token) (*windows.Tokenuser, error) {
|
||||
buf, err := getTokenInfo(token, windows.TokenUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*windows.Tokenuser)(unsafe.Pointer(&buf[0])), nil
|
||||
}
|
||||
|
||||
func getTokenPrimaryGroupInfo(token windows.Token) (*windows.Tokenprimarygroup, error) {
|
||||
buf, err := getTokenInfo(token, windows.TokenPrimaryGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*windows.Tokenprimarygroup)(unsafe.Pointer(&buf[0])), nil
|
||||
}
|
||||
|
||||
type userSids struct {
|
||||
User *windows.SID
|
||||
PrimaryGroup *windows.SID
|
||||
}
|
||||
|
||||
func getCurrentUserSids() (*userSids, error) {
|
||||
token, err := windows.OpenCurrentProcessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer token.Close()
|
||||
|
||||
userInfo, err := getTokenUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primaryGroup, err := getTokenPrimaryGroupInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userSids{userInfo.User.Sid, primaryGroup.PrimaryGroup}, nil
|
||||
}
|
||||
|
||||
// ensureStateDirPerms applies a restrictive ACL to the directory specified by dirPath.
|
||||
// It sets the following security attributes on the directory:
|
||||
// Owner: The user for the current process;
|
||||
@@ -93,7 +37,7 @@ func ensureStateDirPerms(dirPath string) error {
|
||||
}
|
||||
|
||||
// We need the info for our current user as SIDs
|
||||
sids, err := getCurrentUserSids()
|
||||
sids, err := winutil.GetCurrentUserSIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios
|
||||
// +build !ios
|
||||
//go:build !ios && !js
|
||||
// +build !ios,!js
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (windows || freebsd || openbsd || darwin) && !ios
|
||||
//go:build (windows || freebsd || openbsd || darwin) && !ios && !js
|
||||
// +build windows freebsd openbsd darwin
|
||||
// +build !ios
|
||||
// +build !js
|
||||
|
||||
package portlist
|
||||
|
||||
|
||||
@@ -41,11 +41,16 @@ main() {
|
||||
# - ID: the short name of the OS (e.g. "debian", "freebsd")
|
||||
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
|
||||
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
|
||||
# - UBUNTU_CODENAME: if it exists, use instead of VERSION_CODENAME
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|pop|neon|zorin|elementary)
|
||||
ubuntu|pop|neon|zorin)
|
||||
OS="ubuntu"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
|
||||
VERSION="$UBUNTU_CODENAME"
|
||||
else
|
||||
VERSION="$VERSION_CODENAME"
|
||||
fi
|
||||
PACKAGETYPE="apt"
|
||||
# Third-party keyrings became the preferred method of
|
||||
# installation in Ubuntu 20.04.
|
||||
@@ -85,6 +90,16 @@ main() {
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
elementary)
|
||||
OS="ubuntu"
|
||||
VERSION="$UBUNTU_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
if [ "$VERSION_ID" -lt 6 ]; then
|
||||
APT_KEY_TYPE="legacy"
|
||||
else
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
parrot)
|
||||
OS="debian"
|
||||
PACKAGETYPE="apt"
|
||||
@@ -161,6 +176,11 @@ main() {
|
||||
VERSION="$VERSION_ID"
|
||||
PACKAGETYPE="yum"
|
||||
;;
|
||||
xenenterprise)
|
||||
OS="centos"
|
||||
VERSION="$(echo "$VERSION_ID" | cut -f1 -d.)"
|
||||
PACKAGETYPE="yum"
|
||||
;;
|
||||
opensuse-leap)
|
||||
OS="opensuse"
|
||||
VERSION="leap/$VERSION_ID"
|
||||
@@ -294,7 +314,8 @@ main() {
|
||||
fi
|
||||
;;
|
||||
rhel)
|
||||
if [ "$VERSION" != "8" ]
|
||||
if [ "$VERSION" != "8" ] && \
|
||||
[ "$VERSION" != "9" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
|
||||
@@ -51,7 +51,7 @@ var ptyName = func(f *os.File) (string, error) {
|
||||
// On success, it may return a non-nil close func which must be closed to
|
||||
// release the session.
|
||||
// See maybeStartLoginSessionLinux.
|
||||
var maybeStartLoginSession = func(logf logger.Logf, uid uint32, localUser, remoteUser, remoteHost, tty string) (close func() error, err error) {
|
||||
var maybeStartLoginSession = func(logf logger.Logf, ia incubatorArgs) (close func() error, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -62,9 +62,10 @@ var maybeStartLoginSession = func(logf logger.Logf, uid uint32, localUser, remot
|
||||
// exec.CommandContext.
|
||||
func (ss *sshSession) newIncubatorCommand() *exec.Cmd {
|
||||
var (
|
||||
name string
|
||||
args []string
|
||||
isSFTP bool
|
||||
name string
|
||||
args []string
|
||||
isSFTP bool
|
||||
isShell bool
|
||||
)
|
||||
switch ss.Subsystem() {
|
||||
case "sftp":
|
||||
@@ -74,6 +75,7 @@ func (ss *sshSession) newIncubatorCommand() *exec.Cmd {
|
||||
if rawCmd := ss.RawCommand(); rawCmd != "" {
|
||||
args = append(args, "-c", rawCmd)
|
||||
} else {
|
||||
isShell = true
|
||||
args = append(args, "-l") // login shell
|
||||
}
|
||||
default:
|
||||
@@ -107,6 +109,13 @@ func (ss *sshSession) newIncubatorCommand() *exec.Cmd {
|
||||
if isSFTP {
|
||||
incubatorArgs = append(incubatorArgs, "--sftp")
|
||||
} else {
|
||||
if isShell {
|
||||
incubatorArgs = append(incubatorArgs, "--shell")
|
||||
// Currently (2022-05-09) `login` is only used for shells
|
||||
if lp, err := exec.LookPath("login"); err == nil {
|
||||
incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
|
||||
}
|
||||
}
|
||||
incubatorArgs = append(incubatorArgs, "--cmd="+name)
|
||||
if len(args) > 0 {
|
||||
incubatorArgs = append(incubatorArgs, "--")
|
||||
@@ -133,6 +142,41 @@ func (stdRWC) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type incubatorArgs struct {
|
||||
uid uint64
|
||||
gid int
|
||||
groups string
|
||||
localUser string
|
||||
remoteUser string
|
||||
remoteIP string
|
||||
ttyName string
|
||||
hasTTY bool
|
||||
cmdName string
|
||||
isSFTP bool
|
||||
isShell bool
|
||||
loginCmdPath string
|
||||
cmdArgs []string
|
||||
}
|
||||
|
||||
func parseIncubatorArgs(args []string) (a incubatorArgs) {
|
||||
flags := flag.NewFlagSet("", flag.ExitOnError)
|
||||
flags.Uint64Var(&a.uid, "uid", 0, "the uid of local-user")
|
||||
flags.IntVar(&a.gid, "gid", 0, "the gid of local-user")
|
||||
flags.StringVar(&a.groups, "groups", "", "comma-separated list of gids of local-user")
|
||||
flags.StringVar(&a.localUser, "local-user", "", "the user to run as")
|
||||
flags.StringVar(&a.remoteUser, "remote-user", "", "the remote user/tags")
|
||||
flags.StringVar(&a.remoteIP, "remote-ip", "", "the remote Tailscale IP")
|
||||
flags.StringVar(&a.ttyName, "tty-name", "", "the tty name (pts/3)")
|
||||
flags.BoolVar(&a.hasTTY, "has-tty", false, "is the output attached to a tty")
|
||||
flags.StringVar(&a.cmdName, "cmd", "", "the cmd to launch (ignored in sftp mode)")
|
||||
flags.BoolVar(&a.isShell, "shell", false, "is launching a shell (with no cmds)")
|
||||
flags.BoolVar(&a.isSFTP, "sftp", false, "run sftp server (cmd is ignored)")
|
||||
flags.StringVar(&a.loginCmdPath, "login-cmd", "", "the path to `login` cmd")
|
||||
flags.Parse(args)
|
||||
a.cmdArgs = flags.Args()
|
||||
return a
|
||||
}
|
||||
|
||||
// beIncubator is the entrypoint to the `tailscaled be-child ssh` subcommand.
|
||||
// It is responsible for informing the system of a new login session for the user.
|
||||
// This is sometimes necessary for mounting home directories and decrypting file
|
||||
@@ -143,23 +187,10 @@ func (stdRWC) Close() error {
|
||||
// OS, sets its UID and groups to the specified `--uid`, `--gid` and
|
||||
// `--groups` and then launches the requested `--cmd`.
|
||||
func beIncubator(args []string) error {
|
||||
var (
|
||||
flags = flag.NewFlagSet("", flag.ExitOnError)
|
||||
uid = flags.Uint64("uid", 0, "the uid of local-user")
|
||||
gid = flags.Int("gid", 0, "the gid of local-user")
|
||||
groups = flags.String("groups", "", "comma-separated list of gids of local-user")
|
||||
localUser = flags.String("local-user", "", "the user to run as")
|
||||
remoteUser = flags.String("remote-user", "", "the remote user/tags")
|
||||
remoteIP = flags.String("remote-ip", "", "the remote Tailscale IP")
|
||||
ttyName = flags.String("tty-name", "", "the tty name (pts/3)")
|
||||
hasTTY = flags.Bool("has-tty", false, "is the output attached to a tty")
|
||||
cmdName = flags.String("cmd", "", "the cmd to launch (ignored in sftp mode)")
|
||||
sftpMode = flags.Bool("sftp", false, "run sftp server (cmd is ignored)")
|
||||
)
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
ia := parseIncubatorArgs(args)
|
||||
if ia.isSFTP && ia.isShell {
|
||||
return fmt.Errorf("--sftp and --shell are mutually exclusive")
|
||||
}
|
||||
cmdArgs := flags.Args()
|
||||
|
||||
logf := logger.Discard
|
||||
if debugIncubator {
|
||||
@@ -170,16 +201,24 @@ func beIncubator(args []string) error {
|
||||
}
|
||||
|
||||
euid := uint64(os.Geteuid())
|
||||
runningAsRoot := euid == 0
|
||||
if runningAsRoot && ia.isShell && ia.loginCmdPath != "" && ia.hasTTY {
|
||||
// If we are trying to launch a login shell, just exec into login
|
||||
// instead. We can only do this if a TTY was requested, otherwise login
|
||||
// exits immediately, which breaks things likes mosh and VSCode.
|
||||
return unix.Exec(ia.loginCmdPath, []string{ia.loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"}, os.Environ())
|
||||
}
|
||||
|
||||
// Inform the system that we are about to log someone in.
|
||||
// We can only do this if we are running as root.
|
||||
// This is best effort to still allow running on machines where
|
||||
// we don't support starting sessions, e.g. darwin.
|
||||
sessionCloser, err := maybeStartLoginSession(logf, uint32(*uid), *localUser, *remoteUser, *remoteIP, *ttyName)
|
||||
sessionCloser, err := maybeStartLoginSession(logf, ia)
|
||||
if err == nil && sessionCloser != nil {
|
||||
defer sessionCloser()
|
||||
}
|
||||
var groupIDs []int
|
||||
for _, g := range strings.Split(*groups, ",") {
|
||||
for _, g := range strings.Split(ia.groups, ",") {
|
||||
gid, err := strconv.ParseInt(g, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -189,20 +228,20 @@ func beIncubator(args []string) error {
|
||||
if err := syscall.Setgroups(groupIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if egid := os.Getegid(); egid != *gid {
|
||||
if err := syscall.Setgid(int(*gid)); err != nil {
|
||||
if egid := os.Getegid(); egid != ia.gid {
|
||||
if err := syscall.Setgid(int(ia.gid)); err != nil {
|
||||
logf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if euid != *uid {
|
||||
if euid != ia.uid {
|
||||
// Switch users if required before starting the desired process.
|
||||
if err := syscall.Setuid(int(*uid)); err != nil {
|
||||
if err := syscall.Setuid(int(ia.uid)); err != nil {
|
||||
logf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if *sftpMode {
|
||||
if ia.isSFTP {
|
||||
logf("handling sftp")
|
||||
|
||||
server, err := sftp.NewServer(stdRWC{})
|
||||
@@ -212,13 +251,13 @@ func beIncubator(args []string) error {
|
||||
return server.Serve()
|
||||
}
|
||||
|
||||
cmd := exec.Command(*cmdName, cmdArgs...)
|
||||
cmd := exec.Command(ia.cmdName, ia.cmdArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
if *hasTTY {
|
||||
if ia.hasTTY {
|
||||
// If we were launched with a tty then we should
|
||||
// mark that as the ctty of the child. However,
|
||||
// as the ctty is being passed from the parent
|
||||
@@ -266,7 +305,6 @@ func (ss *sshSession) launchProcess() error {
|
||||
return ss.startWithStdPipes()
|
||||
}
|
||||
ss.ptyReq = &ptyReq
|
||||
ss.logf("starting pty command: %+v", cmd.Args)
|
||||
pty, err := ss.startWithPTY()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -443,6 +481,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
|
||||
cmd.Stdout = tty
|
||||
cmd.Stderr = tty
|
||||
|
||||
ss.logf("starting pty command: %+v", cmd.Args)
|
||||
if err = cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,21 +148,21 @@ func releaseSession(sessionID string) error {
|
||||
}
|
||||
|
||||
// maybeStartLoginSessionLinux is the linux implementation of maybeStartLoginSession.
|
||||
func maybeStartLoginSessionLinux(logf logger.Logf, uid uint32, localUser, remoteUser, remoteHost, tty string) (func() error, error) {
|
||||
func maybeStartLoginSessionLinux(logf logger.Logf, ia incubatorArgs) (func() error, error) {
|
||||
if os.Geteuid() != 0 {
|
||||
return nil, nil
|
||||
}
|
||||
logf("starting session for user %d", uid)
|
||||
logf("starting session for user %d", ia.uid)
|
||||
// The only way we can actually start a new session is if we are
|
||||
// running outside one and are root, which is typically the case
|
||||
// for systemd managed tailscaled.
|
||||
resp, err := createSession(uint32(uid), remoteUser, remoteHost, tty)
|
||||
resp, err := createSession(uint32(ia.uid), ia.remoteUser, ia.remoteIP, ia.ttyName)
|
||||
if err != nil {
|
||||
// TODO(maisem): figure out if we are running in a session.
|
||||
// We can look at the DBus GetSessionByPID API.
|
||||
// https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
|
||||
// For now best effort is fine.
|
||||
logf("ssh: failed to CreateSession for user %q (%d) %v", localUser, uid, err)
|
||||
logf("ssh: failed to CreateSession for user %q (%d) %v", ia.localUser, ia.uid, err)
|
||||
return nil, nil
|
||||
}
|
||||
os.Setenv("DBUS_SESSION_BUS_ADDRESS", fmt.Sprintf("unix:path=%v/bus", resp.runtimePath))
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
@@ -58,11 +59,14 @@ type server struct {
|
||||
pubKeyHTTPClient *http.Client // or nil for http.DefaultClient
|
||||
timeNow func() time.Time // or nil for time.Now
|
||||
|
||||
sessionWaitGroup sync.WaitGroup
|
||||
|
||||
// mu protects the following
|
||||
mu sync.Mutex
|
||||
activeSessionByH map[string]*sshSession // ssh.SessionID (DH H) => session
|
||||
activeSessionBySharedID map[string]*sshSession // yyymmddThhmmss-XXXXX => session
|
||||
fetchPublicKeysCache map[string]pubKeyCacheEntry // by https URL
|
||||
shutdownCalled bool
|
||||
}
|
||||
|
||||
func (srv *server) now() time.Time {
|
||||
@@ -89,6 +93,7 @@ func init() {
|
||||
|
||||
// HandleSSHConn handles a Tailscale SSH connection from c.
|
||||
func (srv *server) HandleSSHConn(c net.Conn) error {
|
||||
metricIncomingConnections.Add(1)
|
||||
ss, err := srv.newConn()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -101,6 +106,20 @@ func (srv *server) HandleSSHConn(c net.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown terminates all active sessions.
|
||||
func (srv *server) Shutdown() {
|
||||
srv.mu.Lock()
|
||||
srv.shutdownCalled = true
|
||||
for _, s := range srv.activeSessionByH {
|
||||
s.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Tailscale SSH is shutting down.\r\n"),
|
||||
context.Canceled,
|
||||
})
|
||||
}
|
||||
srv.mu.Unlock()
|
||||
srv.sessionWaitGroup.Wait()
|
||||
}
|
||||
|
||||
// OnPolicyChange terminates any active sessions that no longer match
|
||||
// the SSH access policy.
|
||||
func (srv *server) OnPolicyChange() {
|
||||
@@ -227,6 +246,15 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
}
|
||||
|
||||
func (srv *server) newConn() (*conn, error) {
|
||||
srv.mu.Lock()
|
||||
shutdownCalled := srv.shutdownCalled
|
||||
srv.mu.Unlock()
|
||||
if shutdownCalled {
|
||||
// Stop accepting new connections.
|
||||
// Connections in the auth phase are handled in handleConnPostSSHAuth.
|
||||
// Existing sessions are terminated by Shutdown.
|
||||
return nil, gossh.ErrDenied
|
||||
}
|
||||
c := &conn{srv: srv, now: srv.now()}
|
||||
c.Server = &ssh.Server{
|
||||
Version: "Tailscale",
|
||||
@@ -275,7 +303,11 @@ func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return ss.action.AllowLocalPortForwarding
|
||||
if !ss.action.AllowLocalPortForwarding {
|
||||
return false
|
||||
}
|
||||
metricLocalPortForward.Add(1)
|
||||
return true
|
||||
}
|
||||
|
||||
// havePubKeyPolicy reports whether any policy rule may provide access by means
|
||||
@@ -488,6 +520,9 @@ func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
|
||||
// but not necessarily before all the Tailscale-level extra verification has
|
||||
// completed. It also handles SFTP requests.
|
||||
func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
|
||||
if s.PublicKey() != nil {
|
||||
metricPublicKeyConnections.Add(1)
|
||||
}
|
||||
sshUser := s.User()
|
||||
cr := &contextReader{r: s}
|
||||
action, err := c.resolveTerminalAction(s, cr)
|
||||
@@ -502,6 +537,9 @@ func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if s.PublicKey() != nil {
|
||||
metricPublicKeyAccepts.Add(1)
|
||||
}
|
||||
|
||||
if cr.HasOutstandingRead() {
|
||||
s = contextReaderSesssion{s, cr}
|
||||
@@ -510,8 +548,9 @@ func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
|
||||
// Do this check after auth, but before starting the session.
|
||||
switch s.Subsystem() {
|
||||
case "sftp", "":
|
||||
metricSFTP.Add(1)
|
||||
default:
|
||||
fmt.Fprintf(s.Stderr(), "Unsupported subsystem %q \r\n", s.Subsystem())
|
||||
fmt.Fprintf(s.Stderr(), "Unsupported subsystem %q\r\n", s.Subsystem())
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
@@ -550,12 +589,19 @@ func (c *conn) resolveTerminalAction(s ssh.Session, cr *contextReader) (*tailcfg
|
||||
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
|
||||
}
|
||||
if action.Accept || action.Reject {
|
||||
if action.Reject {
|
||||
metricTerminalReject.Add(1)
|
||||
} else {
|
||||
metricTerminalAccept.Add(1)
|
||||
}
|
||||
return action, nil
|
||||
}
|
||||
url := action.HoldAndDelegate
|
||||
if url == "" {
|
||||
metricTerminalMalformed.Add(1)
|
||||
return nil, errors.New("reached Action that lacked Accept, Reject, and HoldAndDelegate")
|
||||
}
|
||||
metricHolds.Add(1)
|
||||
awaitReadOnce.Do(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -580,7 +626,10 @@ func (c *conn) resolveTerminalAction(s ssh.Session, cr *contextReader) (*tailcfg
|
||||
action, err = c.fetchSSHAction(ctx, url)
|
||||
if err != nil {
|
||||
if sawInterrupt.Get() {
|
||||
metricTerminalInterrupt.Add(1)
|
||||
return nil, fmt.Errorf("aborted by user")
|
||||
} else {
|
||||
metricTerminalFetchError.Add(1)
|
||||
}
|
||||
return nil, fmt.Errorf("fetching SSHAction from %s: %w", url, err)
|
||||
}
|
||||
@@ -681,6 +730,7 @@ func (ss *sshSession) checkStillValid() {
|
||||
if ss.conn.isStillValid(ss.PublicKey()) {
|
||||
return
|
||||
}
|
||||
metricPolicyChangeKick.Add(1)
|
||||
ss.logf("session no longer valid per new SSH policy; closing")
|
||||
ss.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Access revoked.\r\n"),
|
||||
@@ -756,10 +806,10 @@ func (srv *server) getSessionForContext(sctx ssh.Context) (ss *sshSession, ok bo
|
||||
return
|
||||
}
|
||||
|
||||
// startSession registers ss as an active session.
|
||||
func (srv *server) startSession(ss *sshSession) {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
// startSessionLocked registers ss as an active session.
|
||||
// It must be called with srv.mu held.
|
||||
func (srv *server) startSessionLocked(ss *sshSession) {
|
||||
srv.sessionWaitGroup.Add(1)
|
||||
if ss.idH == "" {
|
||||
panic("empty idH")
|
||||
}
|
||||
@@ -778,6 +828,7 @@ func (srv *server) startSession(ss *sshSession) {
|
||||
|
||||
// endSession unregisters s from the list of active sessions.
|
||||
func (srv *server) endSession(ss *sshSession) {
|
||||
defer srv.sessionWaitGroup.Done()
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
delete(srv.activeSessionByH, ss.idH)
|
||||
@@ -842,11 +893,23 @@ var recordSSH = envknob.Bool("TS_DEBUG_LOG_SSH")
|
||||
// It handles ss once it's been accepted and determined
|
||||
// that it should run.
|
||||
func (ss *sshSession) run() {
|
||||
srv := ss.conn.srv
|
||||
srv.startSession(ss)
|
||||
defer srv.endSession(ss)
|
||||
|
||||
metricActiveSessions.Add(1)
|
||||
defer metricActiveSessions.Add(-1)
|
||||
defer ss.ctx.CloseWithError(errSessionDone)
|
||||
srv := ss.conn.srv
|
||||
|
||||
srv.mu.Lock()
|
||||
if srv.shutdownCalled {
|
||||
srv.mu.Unlock()
|
||||
// Do not start any new sessions.
|
||||
fmt.Fprintf(ss, "Tailscale SSH is shutting down\r\n")
|
||||
ss.Exit(1)
|
||||
return
|
||||
}
|
||||
srv.startSessionLocked(ss)
|
||||
srv.mu.Unlock()
|
||||
|
||||
defer srv.endSession(ss)
|
||||
|
||||
if ss.action.SessionDuration != 0 {
|
||||
t := time.AfterFunc(ss.action.SessionDuration, func() {
|
||||
@@ -858,7 +921,7 @@ func (ss *sshSession) run() {
|
||||
defer t.Stop()
|
||||
}
|
||||
|
||||
logf := srv.logf
|
||||
logf := ss.logf
|
||||
lu := ss.conn.localUser
|
||||
localUser := lu.Username
|
||||
|
||||
@@ -909,15 +972,17 @@ func (ss *sshSession) run() {
|
||||
_, err := io.Copy(rec.writer("i", ss.stdin), ss)
|
||||
if err != nil {
|
||||
// TODO: don't log in the success case.
|
||||
logf("ssh: stdin copy: %v", err)
|
||||
logf("stdin copy: %v", err)
|
||||
}
|
||||
ss.stdin.Close()
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(rec.writer("o", ss), ss.stdout)
|
||||
if err != nil {
|
||||
// TODO: don't log in the success case.
|
||||
logf("ssh: stdout copy: %v", err)
|
||||
logf("stdout copy: %v", err)
|
||||
// If we got an error here, it's probably because the client has
|
||||
// disconnected.
|
||||
ss.ctx.CloseWithError(err)
|
||||
}
|
||||
}()
|
||||
// stderr is nil for ptys.
|
||||
@@ -925,8 +990,7 @@ func (ss *sshSession) run() {
|
||||
go func() {
|
||||
_, err := io.Copy(ss.Stderr(), ss.stderr)
|
||||
if err != nil {
|
||||
// TODO: don't log in the success case.
|
||||
logf("ssh: stderr copy: %v", err)
|
||||
logf("stderr copy: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1289,3 +1353,19 @@ func envEq(a, b string) bool {
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
var (
|
||||
metricActiveSessions = clientmetric.NewGauge("ssh_active_sessions")
|
||||
metricIncomingConnections = clientmetric.NewCounter("ssh_incoming_connections")
|
||||
metricPublicKeyConnections = clientmetric.NewCounter("ssh_publickey_connections") // total
|
||||
metricPublicKeyAccepts = clientmetric.NewCounter("ssh_publickey_accepts") // accepted subset of ssh_publickey_connections
|
||||
metricTerminalAccept = clientmetric.NewCounter("ssh_terminalaction_accept")
|
||||
metricTerminalReject = clientmetric.NewCounter("ssh_terminalaction_reject")
|
||||
metricTerminalInterrupt = clientmetric.NewCounter("ssh_terminalaction_interrupt")
|
||||
metricTerminalMalformed = clientmetric.NewCounter("ssh_terminalaction_malformed")
|
||||
metricTerminalFetchError = clientmetric.NewCounter("ssh_terminalaction_fetch_error")
|
||||
metricHolds = clientmetric.NewCounter("ssh_holds")
|
||||
metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick")
|
||||
metricSFTP = clientmetric.NewCounter("ssh_sftp_requests")
|
||||
metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_requests")
|
||||
)
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode --clonefunc=true --output=tailcfg_clone.go
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode --clonefunc
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -477,132 +475,6 @@ type Hostinfo struct {
|
||||
// require changes to Hostinfo.Equal.
|
||||
}
|
||||
|
||||
// View returns a read-only accessor for hi.
|
||||
func (hi *Hostinfo) View() HostinfoView { return HostinfoView{hi} }
|
||||
|
||||
// HostinfoView is a read-only accessor for Hostinfo.
|
||||
// See Hostinfo.
|
||||
type HostinfoView struct {
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Hostinfo
|
||||
}
|
||||
|
||||
func (v HostinfoView) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.ж)
|
||||
}
|
||||
|
||||
func (v *HostinfoView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("HostinfoView is already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
hi := &Hostinfo{}
|
||||
if err := json.Unmarshal(b, hi); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = hi
|
||||
return nil
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying value is not nil.
|
||||
func (v HostinfoView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a deep-copy of the underlying value.
|
||||
func (v HostinfoView) AsStruct() *Hostinfo { return v.ж.Clone() }
|
||||
|
||||
func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion }
|
||||
func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID }
|
||||
func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID }
|
||||
func (v HostinfoView) OS() string { return v.ж.OS }
|
||||
func (v HostinfoView) OSVersion() string { return v.ж.OSVersion }
|
||||
func (v HostinfoView) Package() string { return v.ж.Package }
|
||||
func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel }
|
||||
func (v HostinfoView) Hostname() string { return v.ж.Hostname }
|
||||
func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp }
|
||||
func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode }
|
||||
func (v HostinfoView) GoArch() string { return v.ж.GoArch }
|
||||
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
func (v HostinfoView) RoutableIPs() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.RoutableIPs)
|
||||
}
|
||||
|
||||
func (v HostinfoView) RequestTags() views.Slice[string] {
|
||||
return views.SliceOf(v.ж.RequestTags)
|
||||
}
|
||||
|
||||
func (v HostinfoView) SSH_HostKeys() views.Slice[string] {
|
||||
return views.SliceOf(v.ж.SSH_HostKeys)
|
||||
}
|
||||
|
||||
func (v HostinfoView) Services() ServiceSlice {
|
||||
return ServiceSliceOf(v.ж.Services)
|
||||
}
|
||||
|
||||
func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() }
|
||||
|
||||
// ServiceSlice is a read-only accessor for a slice of Services
|
||||
type ServiceSlice struct {
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж []Service
|
||||
}
|
||||
|
||||
// ServiceSliceOf returns a ServiceSlice for the provided slice.
|
||||
func ServiceSliceOf(x []Service) ServiceSlice { return ServiceSlice{x} }
|
||||
|
||||
// Len returns the length of the slice.
|
||||
func (v ServiceSlice) Len() int { return len(v.ж) }
|
||||
|
||||
// At returns the Service at index `i` of the slice.
|
||||
func (v ServiceSlice) At(i int) Service { return v.ж[i] }
|
||||
|
||||
// Append appends the underlying slice values to dst.
|
||||
func (v ServiceSlice) Append(dst []Service) []Service {
|
||||
return append(dst, v.ж...)
|
||||
}
|
||||
|
||||
// AsSlice returns a copy of underlying slice.
|
||||
func (v ServiceSlice) AsSlice() []Service {
|
||||
return v.Append(v.ж[:0:0])
|
||||
}
|
||||
|
||||
// NetInfoView is a read-only accessor for NetInfo.
|
||||
// See NetInfo.
|
||||
type NetInfoView struct {
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *NetInfo
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying value is not nil.
|
||||
func (v NetInfoView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a deep-copy of the underlying value.
|
||||
func (v NetInfoView) AsStruct() *NetInfo { return v.ж.Clone() }
|
||||
|
||||
func (v NetInfoView) MappingVariesByDestIP() opt.Bool { return v.ж.MappingVariesByDestIP }
|
||||
func (v NetInfoView) HairPinning() opt.Bool { return v.ж.HairPinning }
|
||||
func (v NetInfoView) WorkingIPv6() opt.Bool { return v.ж.WorkingIPv6 }
|
||||
func (v NetInfoView) WorkingUDP() opt.Bool { return v.ж.WorkingUDP }
|
||||
func (v NetInfoView) HavePortMap() bool { return v.ж.HavePortMap }
|
||||
func (v NetInfoView) UPnP() opt.Bool { return v.ж.UPnP }
|
||||
func (v NetInfoView) PMP() opt.Bool { return v.ж.PMP }
|
||||
func (v NetInfoView) PCP() opt.Bool { return v.ж.PCP }
|
||||
func (v NetInfoView) PreferredDERP() int { return v.ж.PreferredDERP }
|
||||
func (v NetInfoView) LinkType() string { return v.ж.LinkType }
|
||||
func (v NetInfoView) String() string { return v.ж.String() }
|
||||
|
||||
// DERPLatencyForEach calls fn for each value in the DERPLatency map.
|
||||
func (v NetInfoView) DERPLatencyForEach(fn func(k string, v float64)) {
|
||||
for k, v := range v.ж.DERPLatency {
|
||||
fn(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// NetInfo contains information about the host's network state.
|
||||
type NetInfo struct {
|
||||
// MappingVariesByDestIP says whether the host's NAT mappings
|
||||
@@ -681,9 +553,6 @@ func (ni *NetInfo) portMapSummary() string {
|
||||
return prefix + conciseOptBool(ni.UPnP, "U") + conciseOptBool(ni.PMP, "M") + conciseOptBool(ni.PCP, "C")
|
||||
}
|
||||
|
||||
// View returns a read-only accessor for ni.
|
||||
func (ni *NetInfo) View() NetInfoView { return NetInfoView{ni} }
|
||||
|
||||
func conciseOptBool(b opt.Bool, trueVal string) string {
|
||||
if b == "" {
|
||||
return "_"
|
||||
@@ -1009,13 +878,20 @@ type MapRequest struct {
|
||||
// start-up before their first real endpoint update.
|
||||
ReadOnly bool `json:",omitempty"`
|
||||
|
||||
// OmitPeers is whether the client is okay with the Peers list
|
||||
// being omitted in the response. (For example, a client on
|
||||
// start up using ReadOnly to get the DERP map.)
|
||||
// OmitPeers is whether the client is okay with the Peers list being omitted
|
||||
// in the response.
|
||||
//
|
||||
// The behavior of OmitPeers being true varies based on Stream and ReadOnly:
|
||||
//
|
||||
// If OmitPeers is true, Stream is false, and ReadOnly is false,
|
||||
// then the server will let clients update their endpoints without
|
||||
// breaking existing long-polling (Stream == true) connections.
|
||||
// In this case, the server can omit the entire response; the client
|
||||
// only checks the HTTP response status code.
|
||||
//
|
||||
// If OmitPeers is true, Stream is false, but ReadOnly is true,
|
||||
// then all the response fields are included. (This is what the client does
|
||||
// when initially fetching the DERP map.)
|
||||
OmitPeers bool `json:",omitempty"`
|
||||
|
||||
// DebugFlags is a list of strings specifying debugging and
|
||||
@@ -1134,7 +1010,7 @@ var FilterAllowAll = []FilterRule{
|
||||
// DNSConfig is the DNS configuration.
|
||||
type DNSConfig struct {
|
||||
// Resolvers are the DNS resolvers to use, in order of preference.
|
||||
Resolvers []dnstype.Resolver `json:",omitempty"`
|
||||
Resolvers []*dnstype.Resolver `json:",omitempty"`
|
||||
|
||||
// Routes maps DNS name suffixes to a set of DNS resolvers to
|
||||
// use. It is used to implement "split DNS" and other advanced DNS
|
||||
@@ -1146,13 +1022,13 @@ type DNSConfig struct {
|
||||
// If the value is an empty slice, that means the suffix should still
|
||||
// be handled by Tailscale's built-in resolver (100.100.100.100), such
|
||||
// as for the purpose of handling ExtraRecords.
|
||||
Routes map[string][]dnstype.Resolver `json:",omitempty"`
|
||||
Routes map[string][]*dnstype.Resolver `json:",omitempty"`
|
||||
|
||||
// FallbackResolvers is like Resolvers, but is only used if a
|
||||
// split DNS configuration is requested in a configuration that
|
||||
// doesn't work yet without explicit default resolvers.
|
||||
// https://github.com/tailscale/tailscale/issues/1743
|
||||
FallbackResolvers []dnstype.Resolver `json:",omitempty"`
|
||||
FallbackResolvers []*dnstype.Resolver `json:",omitempty"`
|
||||
// Domains are the search domains to use.
|
||||
// Search domains must be FQDNs, but *without* the trailing dot.
|
||||
Domains []string `json:",omitempty"`
|
||||
@@ -1228,6 +1104,9 @@ const (
|
||||
// PingICMP performs a ping between two tailscale nodes using ICMP that is
|
||||
// received by the target systems IP stack.
|
||||
PingICMP PingType = "ICMP"
|
||||
// PingPeerAPI performs a ping between two tailscale nodes using ICMP that is
|
||||
// received by the target systems IP stack.
|
||||
PingPeerAPI PingType = "peerapi"
|
||||
)
|
||||
|
||||
// PingRequest with no IP and Types is a request to send an HTTP request to prove the
|
||||
@@ -1595,6 +1474,8 @@ const (
|
||||
|
||||
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
|
||||
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
|
||||
CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available
|
||||
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
|
||||
|
||||
// Inter-node capabilities.
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode -output=tailcfg_clone.go -clonefunc
|
||||
|
||||
package tailcfg
|
||||
|
||||
@@ -194,19 +193,19 @@ func (src *DNSConfig) Clone() *DNSConfig {
|
||||
}
|
||||
dst := new(DNSConfig)
|
||||
*dst = *src
|
||||
dst.Resolvers = make([]dnstype.Resolver, len(src.Resolvers))
|
||||
dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
|
||||
for i := range dst.Resolvers {
|
||||
dst.Resolvers[i] = *src.Resolvers[i].Clone()
|
||||
dst.Resolvers[i] = src.Resolvers[i].Clone()
|
||||
}
|
||||
if dst.Routes != nil {
|
||||
dst.Routes = map[string][]dnstype.Resolver{}
|
||||
dst.Routes = map[string][]*dnstype.Resolver{}
|
||||
for k := range src.Routes {
|
||||
dst.Routes[k] = append([]dnstype.Resolver{}, src.Routes[k]...)
|
||||
dst.Routes[k] = append([]*dnstype.Resolver{}, src.Routes[k]...)
|
||||
}
|
||||
}
|
||||
dst.FallbackResolvers = make([]dnstype.Resolver, len(src.FallbackResolvers))
|
||||
dst.FallbackResolvers = make([]*dnstype.Resolver, len(src.FallbackResolvers))
|
||||
for i := range dst.FallbackResolvers {
|
||||
dst.FallbackResolvers[i] = *src.FallbackResolvers[i].Clone()
|
||||
dst.FallbackResolvers[i] = src.FallbackResolvers[i].Clone()
|
||||
}
|
||||
dst.Domains = append(src.Domains[:0:0], src.Domains...)
|
||||
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
|
||||
@@ -218,9 +217,9 @@ func (src *DNSConfig) Clone() *DNSConfig {
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
|
||||
Resolvers []dnstype.Resolver
|
||||
Routes map[string][]dnstype.Resolver
|
||||
FallbackResolvers []dnstype.Resolver
|
||||
Resolvers []*dnstype.Resolver
|
||||
Routes map[string][]*dnstype.Resolver
|
||||
FallbackResolvers []*dnstype.Resolver
|
||||
Domains []string
|
||||
Proxied bool
|
||||
Nameservers []netaddr.IP
|
||||
|
||||
761
tailcfg/tailcfg_view.go
Normal file
761
tailcfg/tailcfg_view.go
Normal file
@@ -0,0 +1,761 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package tailcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode
|
||||
|
||||
// View returns a readonly view of User.
|
||||
func (p *User) View() UserView {
|
||||
return UserView{ж: p}
|
||||
}
|
||||
|
||||
// UserView provides a read-only view over User.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type UserView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *User
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v UserView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v UserView) AsStruct() *User {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v UserView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *UserView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x User
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v UserView) ID() UserID { return v.ж.ID }
|
||||
func (v UserView) LoginName() string { return v.ж.LoginName }
|
||||
func (v UserView) DisplayName() string { return v.ж.DisplayName }
|
||||
func (v UserView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
||||
func (v UserView) Domain() string { return v.ж.Domain }
|
||||
func (v UserView) Logins() views.Slice[LoginID] { return views.SliceOf(v.ж.Logins) }
|
||||
func (v UserView) Created() time.Time { return v.ж.Created }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _UserViewNeedsRegeneration = User(struct {
|
||||
ID UserID
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
Logins []LoginID
|
||||
Created time.Time
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Node.
|
||||
func (p *Node) View() NodeView {
|
||||
return NodeView{ж: p}
|
||||
}
|
||||
|
||||
// NodeView provides a read-only view over Node.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type NodeView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Node
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v NodeView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v NodeView) AsStruct() *Node {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v NodeView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *NodeView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Node
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v NodeView) ID() NodeID { return v.ж.ID }
|
||||
func (v NodeView) StableID() StableNodeID { return v.ж.StableID }
|
||||
func (v NodeView) Name() string { return v.ж.Name }
|
||||
func (v NodeView) User() UserID { return v.ж.User }
|
||||
func (v NodeView) Sharer() UserID { return v.ж.Sharer }
|
||||
func (v NodeView) Key() key.NodePublic { return v.ж.Key }
|
||||
func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry }
|
||||
func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine }
|
||||
func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey }
|
||||
func (v NodeView) Addresses() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.Addresses) }
|
||||
func (v NodeView) AllowedIPs() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.AllowedIPs) }
|
||||
func (v NodeView) Endpoints() views.Slice[string] { return views.SliceOf(v.ж.Endpoints) }
|
||||
func (v NodeView) DERP() string { return v.ж.DERP }
|
||||
func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo }
|
||||
func (v NodeView) Created() time.Time { return v.ж.Created }
|
||||
func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
|
||||
func (v NodeView) PrimaryRoutes() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.PrimaryRoutes)
|
||||
}
|
||||
func (v NodeView) LastSeen() *time.Time {
|
||||
if v.ж.LastSeen == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.LastSeen
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v NodeView) Online() *bool {
|
||||
if v.ж.Online == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Online
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive }
|
||||
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
||||
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
|
||||
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
|
||||
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _NodeViewNeedsRegeneration = Node(struct {
|
||||
ID NodeID
|
||||
StableID StableNodeID
|
||||
Name string
|
||||
User UserID
|
||||
Sharer UserID
|
||||
Key key.NodePublic
|
||||
KeyExpiry time.Time
|
||||
Machine key.MachinePublic
|
||||
DiscoKey key.DiscoPublic
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo HostinfoView
|
||||
Created time.Time
|
||||
Tags []string
|
||||
PrimaryRoutes []netaddr.IPPrefix
|
||||
LastSeen *time.Time
|
||||
Online *bool
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []string
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Hostinfo.
|
||||
func (p *Hostinfo) View() HostinfoView {
|
||||
return HostinfoView{ж: p}
|
||||
}
|
||||
|
||||
// HostinfoView provides a read-only view over Hostinfo.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type HostinfoView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Hostinfo
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v HostinfoView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v HostinfoView) AsStruct() *Hostinfo {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v HostinfoView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *HostinfoView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Hostinfo
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion }
|
||||
func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID }
|
||||
func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID }
|
||||
func (v HostinfoView) OS() string { return v.ж.OS }
|
||||
func (v HostinfoView) OSVersion() string { return v.ж.OSVersion }
|
||||
func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop }
|
||||
func (v HostinfoView) Package() string { return v.ж.Package }
|
||||
func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel }
|
||||
func (v HostinfoView) Hostname() string { return v.ж.Hostname }
|
||||
func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp }
|
||||
func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode }
|
||||
func (v HostinfoView) GoArch() string { return v.ж.GoArch }
|
||||
func (v HostinfoView) RoutableIPs() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.RoutableIPs)
|
||||
}
|
||||
func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) }
|
||||
func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) }
|
||||
func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() }
|
||||
func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) }
|
||||
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
|
||||
IPNVersion string
|
||||
FrontendLogID string
|
||||
BackendLogID string
|
||||
OS string
|
||||
OSVersion string
|
||||
Desktop opt.Bool
|
||||
Package string
|
||||
DeviceModel string
|
||||
Hostname string
|
||||
ShieldsUp bool
|
||||
ShareeNode bool
|
||||
GoArch string
|
||||
RoutableIPs []netaddr.IPPrefix
|
||||
RequestTags []string
|
||||
Services []Service
|
||||
NetInfo *NetInfo
|
||||
SSH_HostKeys []string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of NetInfo.
|
||||
func (p *NetInfo) View() NetInfoView {
|
||||
return NetInfoView{ж: p}
|
||||
}
|
||||
|
||||
// NetInfoView provides a read-only view over NetInfo.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type NetInfoView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *NetInfo
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v NetInfoView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v NetInfoView) AsStruct() *NetInfo {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v NetInfoView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *NetInfoView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x NetInfo
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v NetInfoView) MappingVariesByDestIP() opt.Bool { return v.ж.MappingVariesByDestIP }
|
||||
func (v NetInfoView) HairPinning() opt.Bool { return v.ж.HairPinning }
|
||||
func (v NetInfoView) WorkingIPv6() opt.Bool { return v.ж.WorkingIPv6 }
|
||||
func (v NetInfoView) WorkingUDP() opt.Bool { return v.ж.WorkingUDP }
|
||||
func (v NetInfoView) HavePortMap() bool { return v.ж.HavePortMap }
|
||||
func (v NetInfoView) UPnP() opt.Bool { return v.ж.UPnP }
|
||||
func (v NetInfoView) PMP() opt.Bool { return v.ж.PMP }
|
||||
func (v NetInfoView) PCP() opt.Bool { return v.ж.PCP }
|
||||
func (v NetInfoView) PreferredDERP() int { return v.ж.PreferredDERP }
|
||||
func (v NetInfoView) LinkType() string { return v.ж.LinkType }
|
||||
|
||||
func (v NetInfoView) DERPLatency() views.Map[string, float64] { return views.MapOf(v.ж.DERPLatency) }
|
||||
func (v NetInfoView) String() string { return v.ж.String() }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _NetInfoViewNeedsRegeneration = NetInfo(struct {
|
||||
MappingVariesByDestIP opt.Bool
|
||||
HairPinning opt.Bool
|
||||
WorkingIPv6 opt.Bool
|
||||
WorkingUDP opt.Bool
|
||||
HavePortMap bool
|
||||
UPnP opt.Bool
|
||||
PMP opt.Bool
|
||||
PCP opt.Bool
|
||||
PreferredDERP int
|
||||
LinkType string
|
||||
DERPLatency map[string]float64
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Login.
|
||||
func (p *Login) View() LoginView {
|
||||
return LoginView{ж: p}
|
||||
}
|
||||
|
||||
// LoginView provides a read-only view over Login.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type LoginView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Login
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v LoginView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v LoginView) AsStruct() *Login {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v LoginView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *LoginView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Login
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v LoginView) ID() LoginID { return v.ж.ID }
|
||||
func (v LoginView) Provider() string { return v.ж.Provider }
|
||||
func (v LoginView) LoginName() string { return v.ж.LoginName }
|
||||
func (v LoginView) DisplayName() string { return v.ж.DisplayName }
|
||||
func (v LoginView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
||||
func (v LoginView) Domain() string { return v.ж.Domain }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _LoginViewNeedsRegeneration = Login(struct {
|
||||
_ structs.Incomparable
|
||||
ID LoginID
|
||||
Provider string
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DNSConfig.
|
||||
func (p *DNSConfig) View() DNSConfigView {
|
||||
return DNSConfigView{ж: p}
|
||||
}
|
||||
|
||||
// DNSConfigView provides a read-only view over DNSConfig.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type DNSConfigView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *DNSConfig
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v DNSConfigView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v DNSConfigView) AsStruct() *DNSConfig {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v DNSConfigView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *DNSConfigView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x DNSConfig
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v DNSConfigView) Resolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
||||
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers)
|
||||
}
|
||||
|
||||
func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] {
|
||||
return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
||||
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t)
|
||||
})
|
||||
}
|
||||
func (v DNSConfigView) FallbackResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
||||
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.FallbackResolvers)
|
||||
}
|
||||
func (v DNSConfigView) Domains() views.Slice[string] { return views.SliceOf(v.ж.Domains) }
|
||||
func (v DNSConfigView) Proxied() bool { return v.ж.Proxied }
|
||||
func (v DNSConfigView) Nameservers() views.Slice[netaddr.IP] { return views.SliceOf(v.ж.Nameservers) }
|
||||
func (v DNSConfigView) PerDomain() bool { return v.ж.PerDomain }
|
||||
func (v DNSConfigView) CertDomains() views.Slice[string] { return views.SliceOf(v.ж.CertDomains) }
|
||||
func (v DNSConfigView) ExtraRecords() views.Slice[DNSRecord] { return views.SliceOf(v.ж.ExtraRecords) }
|
||||
func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] {
|
||||
return views.SliceOf(v.ж.ExitNodeFilteredSet)
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
|
||||
Resolvers []*dnstype.Resolver
|
||||
Routes map[string][]*dnstype.Resolver
|
||||
FallbackResolvers []*dnstype.Resolver
|
||||
Domains []string
|
||||
Proxied bool
|
||||
Nameservers []netaddr.IP
|
||||
PerDomain bool
|
||||
CertDomains []string
|
||||
ExtraRecords []DNSRecord
|
||||
ExitNodeFilteredSet []string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of RegisterResponse.
|
||||
func (p *RegisterResponse) View() RegisterResponseView {
|
||||
return RegisterResponseView{ж: p}
|
||||
}
|
||||
|
||||
// RegisterResponseView provides a read-only view over RegisterResponse.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type RegisterResponseView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *RegisterResponse
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v RegisterResponseView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v RegisterResponseView) AsStruct() *RegisterResponse {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v RegisterResponseView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *RegisterResponseView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x RegisterResponse
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v RegisterResponseView) User() UserView { return v.ж.User.View() }
|
||||
func (v RegisterResponseView) Login() Login { return v.ж.Login }
|
||||
func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired }
|
||||
func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||
func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL }
|
||||
func (v RegisterResponseView) Error() string { return v.ж.Error }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterResponseViewNeedsRegeneration = RegisterResponse(struct {
|
||||
User User
|
||||
Login Login
|
||||
NodeKeyExpired bool
|
||||
MachineAuthorized bool
|
||||
AuthURL string
|
||||
Error string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DERPRegion.
|
||||
func (p *DERPRegion) View() DERPRegionView {
|
||||
return DERPRegionView{ж: p}
|
||||
}
|
||||
|
||||
// DERPRegionView provides a read-only view over DERPRegion.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type DERPRegionView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *DERPRegion
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v DERPRegionView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v DERPRegionView) AsStruct() *DERPRegion {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v DERPRegionView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *DERPRegionView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x DERPRegion
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v DERPRegionView) RegionID() int { return v.ж.RegionID }
|
||||
func (v DERPRegionView) RegionCode() string { return v.ж.RegionCode }
|
||||
func (v DERPRegionView) RegionName() string { return v.ж.RegionName }
|
||||
func (v DERPRegionView) Avoid() bool { return v.ж.Avoid }
|
||||
func (v DERPRegionView) Nodes() views.SliceView[*DERPNode, DERPNodeView] {
|
||||
return views.SliceOfViews[*DERPNode, DERPNodeView](v.ж.Nodes)
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _DERPRegionViewNeedsRegeneration = DERPRegion(struct {
|
||||
RegionID int
|
||||
RegionCode string
|
||||
RegionName string
|
||||
Avoid bool
|
||||
Nodes []*DERPNode
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DERPMap.
|
||||
func (p *DERPMap) View() DERPMapView {
|
||||
return DERPMapView{ж: p}
|
||||
}
|
||||
|
||||
// DERPMapView provides a read-only view over DERPMap.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type DERPMapView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *DERPMap
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v DERPMapView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v DERPMapView) AsStruct() *DERPMap {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v DERPMapView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *DERPMapView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x DERPMap
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v DERPMapView) Regions() views.MapFn[int, *DERPRegion, DERPRegionView] {
|
||||
return views.MapFnOf(v.ж.Regions, func(t *DERPRegion) DERPRegionView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
func (v DERPMapView) OmitDefaultRegions() bool { return v.ж.OmitDefaultRegions }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _DERPMapViewNeedsRegeneration = DERPMap(struct {
|
||||
Regions map[int]*DERPRegion
|
||||
OmitDefaultRegions bool
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DERPNode.
|
||||
func (p *DERPNode) View() DERPNodeView {
|
||||
return DERPNodeView{ж: p}
|
||||
}
|
||||
|
||||
// DERPNodeView provides a read-only view over DERPNode.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type DERPNodeView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *DERPNode
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v DERPNodeView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v DERPNodeView) AsStruct() *DERPNode {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v DERPNodeView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *DERPNodeView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x DERPNode
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v DERPNodeView) Name() string { return v.ж.Name }
|
||||
func (v DERPNodeView) RegionID() int { return v.ж.RegionID }
|
||||
func (v DERPNodeView) HostName() string { return v.ж.HostName }
|
||||
func (v DERPNodeView) CertName() string { return v.ж.CertName }
|
||||
func (v DERPNodeView) IPv4() string { return v.ж.IPv4 }
|
||||
func (v DERPNodeView) IPv6() string { return v.ж.IPv6 }
|
||||
func (v DERPNodeView) STUNPort() int { return v.ж.STUNPort }
|
||||
func (v DERPNodeView) STUNOnly() bool { return v.ж.STUNOnly }
|
||||
func (v DERPNodeView) DERPPort() int { return v.ж.DERPPort }
|
||||
func (v DERPNodeView) InsecureForTests() bool { return v.ж.InsecureForTests }
|
||||
func (v DERPNodeView) STUNTestIP() string { return v.ж.STUNTestIP }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _DERPNodeViewNeedsRegeneration = DERPNode(struct {
|
||||
Name string
|
||||
RegionID int
|
||||
HostName string
|
||||
CertName string
|
||||
IPv4 string
|
||||
IPv6 string
|
||||
STUNPort int
|
||||
STUNOnly bool
|
||||
DERPPort int
|
||||
InsecureForTests bool
|
||||
STUNTestIP string
|
||||
}{})
|
||||
@@ -6,6 +6,7 @@ package vms
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
@@ -53,10 +54,12 @@ var distroData string
|
||||
|
||||
var Distros []Distro = func() []Distro {
|
||||
var result []Distro
|
||||
err := hujson.Unmarshal([]byte(distroData), &result)
|
||||
b, err := hujson.Standardize([]byte(distroData))
|
||||
if err != nil {
|
||||
log.Fatalf("error decoding distros: %v", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &result); err != nil {
|
||||
log.Fatalf("error decoding distros: %v", err)
|
||||
}
|
||||
return result
|
||||
}()
|
||||
|
||||
@@ -64,7 +64,7 @@ func newHarness(t *testing.T) *Harness {
|
||||
// TODO: this is wrong.
|
||||
// It is also only one of many configurations.
|
||||
// Figure out how to scale it up.
|
||||
Resolvers: []dnstype.Resolver{{Addr: "100.100.100.100"}, {Addr: "8.8.8.8"}},
|
||||
Resolvers: []*dnstype.Resolver{{Addr: "100.100.100.100"}, {Addr: "8.8.8.8"}},
|
||||
Domains: []string{"record"},
|
||||
Proxied: true,
|
||||
ExtraRecords: []tailcfg.DNSRecord{{Name: "extratest.record", Type: "A", Value: "1.2.3.4"}},
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file exists just so go mod tidy won't remove
|
||||
// staticcheck's module from our go.mod.
|
||||
// This file exists just so `go mod tidy` won't remove
|
||||
// tool modules from our go.mod.
|
||||
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package tstest
|
||||
package tools
|
||||
|
||||
import (
|
||||
_ "github.com/tailscale/mkctr"
|
||||
_ "honnef.co/go/tools/cmd/staticcheck"
|
||||
)
|
||||
@@ -21,7 +21,9 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -88,7 +90,6 @@ func AllowDebugAccess(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// AcceptsEncoding reports whether r accepts the named encoding
|
||||
// ("gzip", "br", etc).
|
||||
func AcceptsEncoding(r *http.Request, enc string) bool {
|
||||
@@ -161,7 +162,7 @@ func (h Port80Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
host := h.FQDN
|
||||
if host == "" {
|
||||
host = r.URL.Hostname()
|
||||
host = r.Host
|
||||
}
|
||||
target := "https://" + host + path
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
@@ -192,6 +193,10 @@ type HandlerOptions struct {
|
||||
// of status codes for handled responses.
|
||||
// The keys are "1xx", "2xx", "3xx", "4xx", and "5xx".
|
||||
StatusCodeCounters *expvar.Map
|
||||
// If non-nil, StatusCodeCountersFull maintains counters of status
|
||||
// codes for handled responses.
|
||||
// The keys are HTTP numeric response codes e.g. 200, 404, ...
|
||||
StatusCodeCountersFull *expvar.Map
|
||||
}
|
||||
|
||||
// ReturnHandlerFunc is an adapter to allow the use of ordinary
|
||||
@@ -298,11 +303,38 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if h.opts.StatusCodeCounters != nil {
|
||||
key := fmt.Sprintf("%dxx", msg.Code/100)
|
||||
h.opts.StatusCodeCounters.Add(key, 1)
|
||||
h.opts.StatusCodeCounters.Add(responseCodeString(msg.Code/100), 1)
|
||||
}
|
||||
|
||||
if h.opts.StatusCodeCountersFull != nil {
|
||||
h.opts.StatusCodeCountersFull.Add(responseCodeString(msg.Code), 1)
|
||||
}
|
||||
}
|
||||
|
||||
func responseCodeString(code int) string {
|
||||
if v, ok := responseCodeCache.Load(code); ok {
|
||||
return v.(string)
|
||||
}
|
||||
|
||||
var ret string
|
||||
if code < 10 {
|
||||
ret = fmt.Sprintf("%dxx", code)
|
||||
} else {
|
||||
ret = strconv.Itoa(code)
|
||||
}
|
||||
responseCodeCache.Store(code, ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
// responseCodeCache memoizes the string form of HTTP response codes,
|
||||
// so that the hot request-handling codepath doesn't have to allocate
|
||||
// in strconv/fmt for every request.
|
||||
//
|
||||
// Keys are either full HTTP response code ints (200, 404) or "family"
|
||||
// ints representing entire families (e.g. 2 for 2xx codes). Values
|
||||
// are the string form of that code/family.
|
||||
var responseCodeCache sync.Map
|
||||
|
||||
// loggingResponseWriter wraps a ResponseWriter and record the HTTP
|
||||
// response code that gets sent, if any.
|
||||
type loggingResponseWriter struct {
|
||||
|
||||
@@ -580,3 +580,45 @@ func TestAcceptsEncoding(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPort80Handler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
h *Port80Handler
|
||||
req string
|
||||
wantLoc string
|
||||
}{
|
||||
{
|
||||
name: "no_fqdn",
|
||||
h: &Port80Handler{},
|
||||
req: "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n",
|
||||
wantLoc: "https://foo.com/",
|
||||
},
|
||||
{
|
||||
name: "fqdn_and_path",
|
||||
h: &Port80Handler{FQDN: "bar.com"},
|
||||
req: "GET /path HTTP/1.1\r\nHost: foo.com\r\n\r\n",
|
||||
wantLoc: "https://bar.com/path",
|
||||
},
|
||||
{
|
||||
name: "path_and_query_string",
|
||||
h: &Port80Handler{FQDN: "baz.com"},
|
||||
req: "GET /path?a=b HTTP/1.1\r\nHost: foo.com\r\n\r\n",
|
||||
wantLoc: "https://baz.com/path?a=b",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(tt.req)))
|
||||
rec := httptest.NewRecorder()
|
||||
tt.h.ServeHTTP(rec, r)
|
||||
got := rec.Result()
|
||||
if got, want := got.StatusCode, 302; got != want {
|
||||
t.Errorf("got status code %v; want %v", got, want)
|
||||
}
|
||||
if got, want := got.Header.Get("Location"), "https://foo.com/"; got != tt.wantLoc {
|
||||
t.Errorf("Location = %q; want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Package dnstype defines types for working with DNS.
|
||||
package dnstype
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=Resolver --clonefunc=true --output=dnstype_clone.go
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=Resolver --clonefunc=true
|
||||
|
||||
import "inet.af/netaddr"
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Resolver -output=dnstype_clone.go -clonefunc
|
||||
|
||||
package dnstype
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user