Compare commits

..

1 Commits

Author SHA1 Message Date
Brad Fitzpatrick
8b2b679ba3 api.md: add TOC 2021-01-19 12:34:22 -08:00
206 changed files with 3039 additions and 14368 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
id: go
- name: Check out code into the Go module directory

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.x
go-version: 1.15.x
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -38,7 +38,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.16-alpine AS build-env
FROM golang:1.15-alpine AS build-env
WORKDIR /go/src/tailscale
@@ -48,19 +48,7 @@ RUN go mod download
COPY . .
# see build_docker.sh
ARG VERSION_LONG=""
ENV VERSION_LONG=$VERSION_LONG
ARG VERSION_SHORT=""
ENV VERSION_SHORT=$VERSION_SHORT
ARG VERSION_GIT_HASH=""
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
RUN go install -tags=xversion -ldflags="\
-X tailscale.com/version.Long=$VERSION_LONG \
-X tailscale.com/version.Short=$VERSION_SHORT \
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/...
RUN go install -v ./cmd/...
FROM alpine:3.11
RUN apk add --no-cache ca-certificates iptables iproute2

46
LICENSE
View File

@@ -1,29 +1,27 @@
BSD 3-Clause License
Copyright (c) 2020 Tailscale & AUTHORS.
All rights reserved.
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Tailscale Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -12,13 +12,7 @@ depaware:
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
buildwindows:
GOOS=windows GOARCH=amd64 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
build386:
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
check: staticcheck vet depaware buildwindows build386
check: staticcheck vet depaware
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)

View File

@@ -43,7 +43,7 @@ If your distro has conventions that preclude the use of
distro's way, so that bug reports contain useful version information.
We only guarantee to support the latest Go release and any Go beta or
release candidate builds (currently Go 1.16) in module mode. It might
release candidate builds (currently Go 1.15) in module mode. It might
work in earlier Go versions or in GOPATH mode, but we're making no
effort to keep those working.

View File

@@ -1 +1 @@
1.5.0
1.3.0

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for docker distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries inside docker, so that we can track down user
# issues.
#
############################################################################
#
# WARNING: Tailscale is not yet officially supported in Docker,
# Kubernetes, etc.
#
# It might work, but we don't regularly test it, and it's not as polished as
# our currently supported platforms. This is provided for people who know
# how Tailscale works and what they're doing.
#
# Our tracking bug for officially support container use cases is:
# https://github.com/tailscale/tailscale/issues/504
#
# Also, see the various bugs tagged "containers":
# https://github.com/tailscale/tailscale/labels/containers
#
############################################################################
set -eu
eval $(./version/version.sh)
docker build \
--build-arg VERSION_LONG=$VERSION_LONG \
--build-arg VERSION_SHORT=$VERSION_SHORT \
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
-t tailscale:tailscale .

View File

@@ -1,110 +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.
// Package tailscale contains Tailscale client code.
package tailscale
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
)
// tsClient does HTTP requests to the local Tailscale daemon.
var tsClient = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
var d net.Dialer
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
return safesocket.ConnectDefault()
},
},
}
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
//
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
//
// The hostname must be "local-tailscaled.sock", even though it
// doesn't actually do any DNS lookup. The actual means of connecting to and
// authenticating to the local Tailscale daemon vary by platform.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func DoLocalRequest(req *http.Request) (*http.Response, error) {
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token)
}
return tsClient.Do(req)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) {
var ip string
if net.ParseIP(remoteAddr) != nil {
ip = remoteAddr
} else {
var err error
ip, _, err = net.SplitHostPort(remoteAddr)
if err != nil {
return nil, fmt.Errorf("invalid remoteAddr %q", remoteAddr)
}
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
slurp, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
}
r := new(tailcfg.WhoIsResponse)
if err := json.Unmarshal(slurp, r); err != nil {
if max := 200; len(slurp) > max {
slurp = slurp[:max]
}
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
}
return r, nil
}
func Goroutines(ctx context.Context) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/goroutines", nil)
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return body, nil
}

View File

@@ -1,69 +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.
package main
import (
"context"
"encoding/json"
"expvar"
"log"
"net"
"net/http"
"strings"
"sync"
"time"
)
var (
dnsMu sync.Mutex
dnsCache = map[string][]net.IP{}
)
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
func refreshBootstrapDNSLoop() {
if *bootstrapDNS == "" {
return
}
for {
refreshBootstrapDNS()
time.Sleep(10 * time.Minute)
}
}
func refreshBootstrapDNS() {
if *bootstrapDNS == "" {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
names := strings.Split(*bootstrapDNS, ",")
var r net.Resolver
for _, name := range names {
addrs, err := r.LookupIP(ctx, "ip", name)
if err != nil {
log.Printf("bootstrap DNS lookup %q: %v", name, err)
continue
}
dnsMu.Lock()
dnsCache[name] = addrs
dnsMu.Unlock()
}
}
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
bootstrapDNSRequests.Add(1)
dnsMu.Lock()
j, err := json.MarshalIndent(dnsCache, "", "\t")
dnsMu.Unlock()
if err != nil {
log.Printf("bootstrap DNS JSON: %v", err)
http.Error(w, "JSON marshal error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}

View File

@@ -48,7 +48,6 @@ var (
runSTUN = flag.Bool("stun", false, "also run a STUN server")
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
)
type config struct {
@@ -146,8 +145,6 @@ func main() {
// Create our own mux so we don't expose /debug/ stuff to the world.
mux := tsweb.NewMux(debugHandler(s))
mux.Handle("/derp", derphttp.Handler(s))
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
@@ -156,7 +153,7 @@ func main() {
<p>
This is a
<a href="https://tailscale.com/">Tailscale</a>
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
<a href="https://godoc.org/tailscale.com/derp">DERP</a>
server.
</p>
`)

View File

@@ -5,7 +5,6 @@
package main
import (
"context"
"errors"
"fmt"
"log"
@@ -41,6 +40,6 @@ func startMeshWithHost(s *derp.Server, host string) error {
c.MeshKey = s.MeshKey()
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
return nil
}

View File

@@ -1,169 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The hello binary runs hello.ipn.dev.
package main // import "tailscale.com/cmd/hello"
import (
"context"
_ "embed"
"encoding/json"
"flag"
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strings"
"tailscale.com/client/tailscale"
)
var (
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
)
//go:embed hello.tmpl.html
var embeddedTemplate string
func main() {
flag.Parse()
if *testIP != "" {
res, err := tailscale.WhoIs(context.Background(), *testIP)
if err != nil {
log.Fatal(err)
}
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
e.Encode(res)
return
}
if devMode() {
// Parse it optimistically
var err error
tmpl, err = template.New("home").Parse(embeddedTemplate)
if err != nil {
log.Printf("ignoring template error in dev mode: %v", err)
}
} else {
if embeddedTemplate == "" {
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
}
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
}
http.HandleFunc("/", root)
log.Printf("Starting hello server.")
errc := make(chan error, 1)
if *httpAddr != "" {
log.Printf("running HTTP server on %s", *httpAddr)
go func() {
errc <- http.ListenAndServe(*httpAddr, nil)
}()
}
if *httpsAddr != "" {
log.Printf("running HTTPS server on %s", *httpsAddr)
go func() {
errc <- http.ListenAndServeTLS(*httpsAddr,
"/etc/hello/hello.ipn.dev.crt",
"/etc/hello/hello.ipn.dev.key",
nil,
)
}()
}
log.Fatal(<-errc)
}
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
if os.IsNotExist(err) {
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
return tmpl, nil
}
return template.New("home").Parse(string(tmplData))
}
return tmpl, nil
}
// tmpl is the template used in prod mode.
// In dev mode it's only used if the template file doesn't exist on disk.
// It's initialized by main after flag parsing.
var tmpl *template.Template
type tmplData struct {
DisplayName string // "Foo Barberson"
LoginName string // "foo@bar.com"
ProfilePicURL string // "https://..."
MachineName string // "imac5k"
MachineOS string // "Linux"
IP string // "100.2.3.4"
}
func root(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && *httpsAddr != "" {
host := r.Host
if strings.Contains(r.Host, "100.101.102.103") {
host = "hello.ipn.dev"
}
http.Redirect(w, r, "https://"+host, http.StatusFound)
return
}
if r.RequestURI != "/" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
tmpl, err := getTmpl()
if err != nil {
w.Header().Set("Content-Type", "text/plain")
http.Error(w, "template error: "+err.Error(), 500)
return
}
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
var data tmplData
if err != nil {
if devMode() {
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
data = tmplData{
DisplayName: "Taily Scalerson",
LoginName: "taily@scaler.son",
ProfilePicURL: "https://placekitten.com/200/200",
MachineName: "scaled",
MachineOS: "Linux",
IP: "100.1.2.3",
}
} else {
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
return
}
} else {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
data = tmplData{
DisplayName: who.UserProfile.DisplayName,
LoginName: who.UserProfile.LoginName,
ProfilePicURL: who.UserProfile.ProfilePicURL,
MachineName: firstLabel(who.Node.ComputedName),
MachineOS: who.Node.Hostinfo.OS,
IP: ip,
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}
// firstLabel s up until the first period, if any.
func firstLabel(s string) string {
if i := strings.Index(s, "."); i != -1 {
return s[:i]
}
return s
}

View File

@@ -1,436 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Hello from Tailscale</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body,
main {
height: 100%;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #dad6d5;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
font-size: 1rem;
font-weight: inherit;
}
a {
color: inherit;
}
p {
margin: 0;
}
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 24rem;
width: 95%;
margin-left: auto;
margin-right: auto;
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.pl-3 {
padding-left: 0.75rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.pt-4 {
padding-top: 1rem;
}
.mr-2 {
margin-right: 0.5rem;
;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mb-12 {
margin-bottom: 3rem;
}
.width-full {
width: 100%;
}
.min-width-0 {
min-width: 0;
}
.rounded-lg {
border-radius: 0.5rem;
}
.relative {
position: relative;
}
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
.border {
border-width: 1px;
}
.border-t-1 {
border-top-width: 1px;
}
.border-gray-100 {
border-color: #f7f5f4;
}
.border-gray-200 {
border-color: #eeebea;
}
.border-gray-300 {
border-color: #dad6d5;
}
.bg-white {
background-color: white;
}
.bg-gray-0 {
background-color: #faf9f8;
}
.bg-gray-100 {
background-color: #f7f5f4;
}
.text-green-600 {
color: #0d4b3b;
}
.text-blue-600 {
color: #3f5db3;
}
.hover\:text-blue-800:hover {
color: #253570;
}
.text-gray-600 {
color: #444342;
}
.text-gray-700 {
color: #2e2d2d;
}
.text-gray-800 {
color: #232222;
}
.text-center {
text-align: center;
}
.text-sm {
font-size: 0.875rem;
}
.font-title {
font-size: 1.25rem;
letter-spacing: -0.025em;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.font-regular {
font-weight: 400;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.profile-pic {
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
background-size: cover;
margin-right: 0.5rem;
flex-shrink: 0;
}
.panel {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.animate .panel {
transform: translateY(10%);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.0), 0 10px 10px -5px rgba(0, 0, 0, 0.0);
transition: transform 1200ms ease, opacity 1200ms ease, box-shadow 1200ms ease;
}
.animate .panel-interior {
opacity: 0.0;
transition: opacity 1200ms ease;
}
.animate .logo {
transform: translateY(2rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .header-title {
transform: translateY(1.6rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .header-text {
transform: translateY(1.2rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .footer {
transform: translateY(-0.5rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animating .panel {
transform: translateY(0);
opacity: 1.0;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.animating .panel-interior {
opacity: 1.0;
}
.animating .spinner {
opacity: 0.0;
}
.animating .logo,
.animating .header-title,
.animating .header-text,
.animating .footer {
transform: translateY(0);
opacity: 1.0;
}
.spinner {
display: inline-flex;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
align-items: center;
transition: opacity 200ms ease;
}
.spinner span {
display: inline-block;
background-color: currentColor;
border-radius: 9999px;
animation-name: loading-dots-blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: 0.35em;
height: 0.35em;
margin: 0 0.15em;
}
.spinner span:nth-child(2) {
animation-delay: 200ms;
}
.spinner span:nth-child(3) {
animation-delay: 400ms;
}
.spinner {
display: none;
}
.animate .spinner {
display: inline-flex;
}
@keyframes loading-dots-blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion) {
* {
animation-duration: 0ms !important;
transition-duration: 0ms !important;
transition-delay: 0ms !important;
}
}
</style>
</head>
<body class="bg-gray-100">
<script>
(function() {
var lastSeen = localStorage.getItem("lastSeen");
if (!lastSeen) {
document.body.classList.add("animate");
window.addEventListener("load", function () {
setTimeout(function () {
document.body.classList.add("animating");
localStorage.setItem("lastSeen", Date.now());
}, 100);
});
}
})();
</script>
<main class="text-gray-800">
<svg class="logo mb-6" width="28" height="28" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor" />
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor" />
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor" />
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor" />
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor" />
</svg>
<header class="mb-8 text-center">
<h1 class="header-title font-title font-semibold mb-2">You're connected over Tailscale!</h1>
<p class="header-text">This device is signed in as…</p>
</header>
<div class="panel relative bg-white rounded-lg width-full shadow-xl mb-8 p-4">
<div class="spinner text-gray-600">
<span></span>
<span></span>
<span></span>
</div>
<div class="panel-interior flex items-center width-full min-width-0 p-2 mb-4">
<div class="profile-pic bg-gray-100" style="background-image: url({{.ProfilePicURL}});"></div>
<div class="overflow-hidden">
{{ with .DisplayName }}
<h4 class="font-semibold truncate">{{.}}</h4>
{{ end }}
<h5 class="text-gray-600 truncate">{{.LoginName}}</h5>
</div>
</div>
<div
class="panel-interior border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-2 width-full flex justify-between items-center">
<div class="flex items-center min-width-0">
<svg class="text-gray-600 mr-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 class="font-semibold truncate mr-2">{{.MachineName}}</h4>
</div>
<h5>{{.IP}}</h5>
</div>
</div>
<footer class="footer text-gray-600 text-center mb-12">
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
target="_blank">what you can do next &rarr;</a></p>
</footer>
</main>
</body>
</html>

View File

@@ -50,13 +50,9 @@ func Run(args []string) error {
rootCmd := &ffcli.Command{
Name: "tailscale",
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
ShortUsage: "tailscale subcommand [flags]",
ShortHelp: "The easiest, most secure way to use WireGuard.",
LongHelp: strings.TrimSpace(`
For help on subcommands, add -help after: "tailscale status -help".
All flags can use single or double hyphen prefixes (-help or --help).
This CLI is still under active development. Commands and flags will
change in the future.
`),

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// 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.
@@ -6,12 +6,26 @@ package cli
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"time"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/derp/derphttp"
"tailscale.com/derp/derpmap"
"tailscale.com/net/interfaces"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/wgengine/monitor"
)
var debugCmd = &ffcli.Command{
@@ -19,25 +33,143 @@ var debugCmd = &ffcli.Command{
Exec: runDebug,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
return fs
})(),
}
var debugArgs struct {
goroutines bool
monitor bool
getURL string
derpCheck string
}
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if debugArgs.goroutines {
goroutines, err := tailscale.Goroutines(ctx)
if err != nil {
return err
}
os.Stdout.Write(goroutines)
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}
return nil
if debugArgs.monitor {
return runMonitor(ctx)
}
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
}
return errors.New("only --monitor is available at the moment")
}
func runMonitor(ctx context.Context) error {
dump := func() {
st, err := interfaces.GetState()
if err != nil {
log.Printf("error getting state: %v", err)
return
}
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := monitor.New(log.Printf, func() {
log.Printf("Link monitor fired. State:")
dump()
})
if err != nil {
return err
}
log.Printf("Starting link change monitor; initial state:")
dump()
mon.Start()
log.Printf("Started link change monitor; waiting...")
select {}
}
func getURL(ctx context.Context, urlStr string) error {
if urlStr == "login" {
urlStr = "https://login.tailscale.com"
}
log.SetOutput(os.Stdout)
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
})
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil {
return fmt.Errorf("http.NewRequestWithContext: %v", err)
}
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
if err != nil {
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
}
log.Printf("proxy: %v", proxyURL)
tr := &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
ProxyConnectHeader: http.Header{},
DisableKeepAlives: true,
}
if proxyURL != nil {
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
if err == nil && auth != "" {
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
}
const truncLen = 20
if len(auth) > truncLen {
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
}
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
}
res, err := tr.RoundTrip(req)
if err != nil {
return fmt.Errorf("Transport.RoundTrip: %v", err)
}
defer res.Body.Close()
return res.Write(os.Stdout)
}
func checkDerp(ctx context.Context, derpRegion string) error {
dmap := derpmap.Prod()
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
return r
}
}
for _, r := range dmap.Regions {
log.Printf("Known region: %q", r.RegionCode)
}
log.Fatalf("unknown region %q", derpRegion)
panic("unreachable")
}
priv1 := key.NewPrivate()
priv2 := key.NewPrivate()
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
c2.NotePreferred(true) // just to open it
m, err := c2.Recv()
log.Printf("c2 got %T, %v", m, err)
t0 := time.Now()
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
return err
}
fmt.Println(time.Since(t0))
m, err = c2.Recv()
log.Printf("c2 got %T, %v", m, err)
if err != nil {
return err
}
log.Printf("ok")
return err
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derpmap"
"tailscale.com/net/netcheck"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
)
@@ -44,10 +43,7 @@ var netcheckArgs struct {
}
func runNetcheck(ctx context.Context, args []string) error {
c := &netcheck.Client{
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: ")),
}
c := &netcheck.Client{}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
c.Verbose = true

View File

@@ -64,63 +64,34 @@ func runPing(ctx context.Context, args []string) error {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
if len(args) != 1 || args[0] == "" {
if len(args) != 1 {
return errors.New("usage: ping <hostname-or-IP>")
}
hostOrIP := args[0]
var ip string
prc := make(chan *ipnstate.PingResult, 1)
stc := make(chan *ipnstate.Status, 1)
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
ch := make(chan *ipnstate.PingResult, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
prc <- pr
}
if n.Status != nil {
stc <- n.Status
ch <- pr
}
})
go pump(ctx, bc, c)
hostOrIP := args[0]
// If the argument is an IP address, use it directly without any resolution.
if net.ParseIP(hostOrIP) != nil {
ip = hostOrIP
}
// Otherwise, try to resolve it first from the network peer list.
if ip == "" {
bc.RequestStatus()
select {
case st := <-stc:
for _, ps := range st.Peer {
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
ip = ps.TailAddr
break
}
}
case <-ctx.Done():
return ctx.Err()
}
}
// Finally, use DNS.
if ip == "" {
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
n := 0
anyPong := false
for {
@@ -130,7 +101,7 @@ func runPing(ctx context.Context, args []string) error {
select {
case <-timer.C:
fmt.Printf("timeout waiting for ping reply\n")
case pr := <-prc:
case pr := <-ch:
timer.Stop()
if pr.Err != "" {
return errors.New(pr.Err)

View File

@@ -14,6 +14,7 @@ import (
"net"
"net/http"
"os"
"sort"
"strings"
"time"
@@ -37,7 +38,7 @@ var statusCmd = &ffcli.Command{
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
return fs
})(),
@@ -65,17 +66,7 @@ func runStatus(ctx context.Context, args []string) error {
log.Fatal(*n.ErrMessage)
}
if n.Status != nil {
select {
case ch <- n.Status:
default:
// A status update from somebody else's request.
// Ignoring this matters mostly for "tailscale status -web"
// mode, otherwise the channel send would block forever
// and pump would stop reading from tailscaled, which
// previously caused tailscaled to block (while holding
// a mutex), backing up unrelated clients.
// See https://github.com/tailscale/tailscale/issues/1234
}
ch <- n.Status
}
})
go pump(ctx, bc, c)
@@ -159,18 +150,13 @@ func runStatus(ctx context.Context, args []string) error {
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
if !active {
if ps.ExitNode {
f("idle; exit node")
} else if anyTraffic {
if anyTraffic {
f("idle")
} else {
f("-")
}
} else {
f("active; ")
if ps.ExitNode {
f("exit node; ")
}
if relay != "" && ps.CurAddr == "" {
f("relay %q", relay)
} else if ps.CurAddr != "" {
@@ -195,7 +181,7 @@ func runStatus(ctx context.Context, args []string) error {
}
peers = append(peers, ps)
}
ipnstate.SortPeers(peers)
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
for _, ps := range peers {
active := peerActive(ps)
if statusArgs.active && !active {
@@ -216,11 +202,23 @@ func peerActive(ps *ipnstate.PeerStatus) bool {
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {
return baseName
if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) {
return ps.DNSName[:i]
}
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
if ps.DNSName != "" {
return strings.TrimRight(ps.DNSName, ".")
}
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
}
func sortKey(ps *ipnstate.PeerStatus) string {
if ps.DNSName != "" {
return ps.DNSName
}
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
}
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {

View File

@@ -14,7 +14,6 @@ import (
"os"
"os/exec"
"runtime"
"sort"
"strconv"
"strings"
"sync"
@@ -23,8 +22,9 @@ import (
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine/router"
)
var upCmd = &ffcli.Command{
@@ -45,16 +45,13 @@ specify any flags, options are reset to their default.
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
upf.BoolVar(&upArgs.json, "json", false, "print output as JSON and exit when control server provides instructions")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) {
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
}
if runtime.GOOS == "linux" {
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
@@ -73,21 +70,18 @@ func defaultNetfilterMode() string {
}
var upArgs struct {
server string
acceptRoutes bool
acceptDNS bool
singleRoutes bool
exitNodeIP string
shieldsUp bool
forceReauth bool
json bool
advertiseRoutes string
advertiseDefaultRoute bool
advertiseTags string
snat bool
netfilterMode string
authKey string
hostname string
server string
acceptRoutes bool
acceptDNS bool
singleRoutes bool
shieldsUp bool
forceReauth bool
advertiseRoutes string
advertiseTags string
snat bool
netfilterMode string
authKey string
hostname string
}
func isBSD(s string) bool {
@@ -98,14 +92,14 @@ func warnf(format string, args ...interface{}) {
fmt.Printf("Warning: "+format+"\n", args...)
}
// checkIPForwarding prints warnings on linux if IP forwarding is not
// checkIPForwarding prints warnings if IP forwarding is not
// enabled, or if we were unable to verify the state of IP forwarding.
func checkIPForwarding() {
var key string
if runtime.GOOS == "linux" {
key = "net.ipv4.ip_forward"
} else if isBSD(runtime.GOOS) {
} else if isBSD(runtime.GOOS) || version.OS() == "macOS" {
key = "net.inet.ip.forwarding"
} else {
return
@@ -126,11 +120,6 @@ func checkIPForwarding() {
}
}
var (
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6default = netaddr.MustParseIPPrefix("::/0")
)
func runUp(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
@@ -144,16 +133,12 @@ func runUp(ctx context.Context, args []string) error {
if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported)
}
if upArgs.exitNodeIP != "" {
return errors.New("--exit-node is " + notSupported)
}
if upArgs.netfilterMode != "off" {
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
}
}
routeMap := map[netaddr.IPPrefix]bool{}
var default4, default6 bool
var routes []netaddr.IPPrefix
if upArgs.advertiseRoutes != "" {
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
for _, s := range advroutes {
@@ -164,45 +149,10 @@ func runUp(ctx context.Context, args []string) error {
if ipp != ipp.Masked() {
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
if ipp == ipv4default {
default4 = true
} else if ipp == ipv6default {
default6 = true
}
routeMap[ipp] = true
routes = append(routes, ipp)
}
if default4 && !default6 {
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
} else if default6 && !default4 {
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
}
}
if upArgs.advertiseDefaultRoute {
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
}
if len(routeMap) > 0 {
checkIPForwarding()
}
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
for r := range routeMap {
routes = append(routes, r)
}
sort.Slice(routes, func(i, j int) bool {
if routes[i].Bits != routes[j].Bits {
return routes[i].Bits < routes[j].Bits
}
return routes[i].IP.Less(routes[j].IP)
})
var exitNodeIP netaddr.IP
if upArgs.exitNodeIP != "" {
var err error
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
if err != nil {
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
}
}
var tags []string
if upArgs.advertiseTags != "" {
@@ -219,11 +169,11 @@ func runUp(ctx context.Context, args []string) error {
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
}
// TODO(apenwarr): fix different semantics between prefs and uflags
prefs := ipn.NewPrefs()
prefs.ControlURL = upArgs.server
prefs.WantRunning = true
prefs.RouteAll = upArgs.acceptRoutes
prefs.ExitNodeIP = exitNodeIP
prefs.CorpDNS = upArgs.acceptDNS
prefs.AllowSingleHosts = upArgs.singleRoutes
prefs.ShieldsUp = upArgs.shieldsUp
@@ -236,12 +186,12 @@ func runUp(ctx context.Context, args []string) error {
if runtime.GOOS == "linux" {
switch upArgs.netfilterMode {
case "on":
prefs.NetfilterMode = preftype.NetfilterOn
prefs.NetfilterMode = router.NetfilterOn
case "nodivert":
prefs.NetfilterMode = preftype.NetfilterNoDivert
prefs.NetfilterMode = router.NetfilterNoDivert
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
case "off":
prefs.NetfilterMode = preftype.NetfilterOff
prefs.NetfilterMode = router.NetfilterOff
warnf("netfilter=off; configure iptables yourself.")
default:
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
@@ -262,16 +212,7 @@ func runUp(ctx context.Context, args []string) error {
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
msg += " (try 'sudo tailscale up [...]')"
}
}
fatalf("backend error: %v\n", msg)
fatalf("backend error: %v\n", *n.ErrMessage)
}
if s := n.State; s != nil {
switch *s {
@@ -296,29 +237,6 @@ func runUp(ctx context.Context, args []string) error {
},
}
if upArgs.json {
opts.Notify = func(n ipn.Notify) {
if n.ErrMessage != nil {
fmt.Fprintf(os.Stdout, `{"error":%q}`, *n.ErrMessage)
os.Exit(1)
}
state := ""
if n.State != nil {
state = n.State.String()
}
if url := n.BrowseToURL; url != nil {
fmt.Fprintf(os.Stdout, `{"state":%q,"auth_url":%q}`, state, *url)
cancel()
return
}
if n.State != nil && *n.State == ipn.Running {
fmt.Fprintf(os.Stdout, `{"state":%q}`, state)
cancel()
return
}
}
}
// On Windows, we still run in mostly the "legacy" way that
// predated the server's StateStore. That is, we send an empty
// StateKey and send the prefs directly. Although the Windows

View File

@@ -2,85 +2,124 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
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/sdnotify from tailscale.com/util/systemd
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/derp+
💣 go4.org/mem from tailscale.com/control/controlclient+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
tailscale.com/disco from tailscale.com/derp
tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
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/netns from tailscale.com/derp/derphttp+
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/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/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/packet from tailscale.com/wgengine+
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/tshttpproxy from tailscale.com/cmd/tailscale/cli+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
tailscale.com/portlist from tailscale.com/ipn
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+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/wgkey from tailscale.com/types/netmap+
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/lineread from tailscale.com/net/interfaces
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/systemd from tailscale.com/control/controlclient+
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
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine from tailscale.com/ipn
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
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/nacl/box from tailscale.com/derp
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from net
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/oauth2 from tailscale.com/ipn+
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
@@ -89,7 +128,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from net/http
compress/gzip from net/http+
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
@@ -117,7 +156,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
encoding from encoding/json
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
@@ -131,16 +170,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnstate
io from bufio+
io/fs from crypto/rand+
io/ioutil from golang.org/x/oauth2/internal+
io/ioutil from crypto/tls+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from math/big+
math/rand from github.com/mdlayher/netlink+
mime from golang.org/x/oauth2/internal+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
@@ -151,21 +190,23 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/toqueteos/webbrowser+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscale/cli
L os/user from github.com/godbus/dbus/v5
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from rsc.io/goversion/version
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight
runtime/pprof from tailscale.com/log/logheap+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+

View File

@@ -8,12 +8,19 @@ package main // import "tailscale.com/cmd/tailscale"
import (
"fmt"
"log"
"os"
"github.com/apenwarr/fixconsole"
"tailscale.com/cmd/tailscale/cli"
)
func main() {
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Printf("fixConsoleOutput: %v\n", err)
}
if err := cli.Run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)

View File

@@ -1,172 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"time"
"tailscale.com/derp/derphttp"
"tailscale.com/derp/derpmap"
"tailscale.com/net/interfaces"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/wgengine/monitor"
)
var debugArgs struct {
monitor bool
getURL string
derpCheck string
}
var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
if err := fs.Parse(args); err != nil {
return err
}
if len(fs.Args()) > 0 {
return errors.New("unknown non-flag debug subcommand arguments")
}
ctx := context.Background()
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}
if debugArgs.monitor {
return runMonitor(ctx)
}
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
}
return errors.New("only --monitor is available at the moment")
}
func runMonitor(ctx context.Context) error {
dump := func(st *interfaces.State) {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := monitor.New(log.Printf)
if err != nil {
return err
}
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if !changed {
log.Printf("Link monitor fired; no change")
return
}
log.Printf("Link monitor fired. New state:")
dump(st)
})
log.Printf("Starting link change monitor; initial state:")
dump(mon.InterfaceState())
mon.Start()
log.Printf("Started link change monitor; waiting...")
select {}
}
func getURL(ctx context.Context, urlStr string) error {
if urlStr == "login" {
urlStr = "https://login.tailscale.com"
}
log.SetOutput(os.Stdout)
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
})
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil {
return fmt.Errorf("http.NewRequestWithContext: %v", err)
}
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
if err != nil {
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
}
log.Printf("proxy: %v", proxyURL)
tr := &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
ProxyConnectHeader: http.Header{},
DisableKeepAlives: true,
}
if proxyURL != nil {
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
if err == nil && auth != "" {
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
}
const truncLen = 20
if len(auth) > truncLen {
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
}
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
}
res, err := tr.RoundTrip(req)
if err != nil {
return fmt.Errorf("Transport.RoundTrip: %v", err)
}
defer res.Body.Close()
return res.Write(os.Stdout)
}
func checkDerp(ctx context.Context, derpRegion string) error {
dmap := derpmap.Prod()
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
return r
}
}
for _, r := range dmap.Regions {
log.Printf("Known region: %q", r.RegionCode)
}
log.Fatalf("unknown region %q", derpRegion)
panic("unreachable")
}
priv1 := key.NewPrivate()
priv2 := key.NewPrivate()
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
c2.NotePreferred(true) // just to open it
m, err := c2.Recv()
log.Printf("c2 got %T, %v", m, err)
t0 := time.Now()
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
return err
}
fmt.Println(time.Since(t0))
m, err = c2.Recv()
log.Printf("c2 got %T, %v", m, err)
if err != nil {
return err
}
log.Printf("ok")
return err
}

View File

@@ -2,13 +2,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
@@ -21,6 +22,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
@@ -29,6 +31,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
@@ -49,11 +52,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/link/channel+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/network/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
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+
@@ -65,22 +67,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+
inet.af/netaddr from tailscale.com/control/controlclient+
inet.af/peercred from tailscale.com/ipn/ipnserver
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/ipn/ipnserver
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
@@ -89,42 +86,33 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/logtail/filch from tailscale.com/logpolicy
tailscale.com/metrics from tailscale.com/derp
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/wgengine/filter+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/packet from tailscale.com/wgengine+
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
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/ipnlocal+
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/paths from tailscale.com/cmd/tailscaled+
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
tailscale.com/syncs from tailscale.com/net/interfaces+
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/control/controlclient+
W 💣 tailscale.com/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
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+
tailscale.com/types/pad32 from tailscale.com/wgengine/magicsock
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
@@ -135,15 +123,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/router/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/tsdns from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
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
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
@@ -167,16 +152,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled
golang.org/x/term from tailscale.com/logpolicy
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
@@ -229,12 +213,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from net/http/pprof+
html from html/template+
html/template from net/http/pprof
io from bufio+
io/fs from crypto/rand+
io/ioutil from github.com/godbus/dbus/v5+
io/ioutil from crypto/tls+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
@@ -269,6 +253,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+

View File

@@ -1,146 +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.
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
)
func init() {
installSystemDaemon = installSystemDaemonDarwin
uninstallSystemDaemon = uninstallSystemDaemonDarwin
}
// darwinLaunchdPlist is the launchd.plist that's written to
// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
// future) a user-specific location.
//
// See man launchd.plist.
const darwinLaunchdPlist = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.tailscale.tailscaled</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/tailscaled</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
`
const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
const targetBin = "/usr/local/bin/tailscaled"
const service = "com.tailscale.tailscaled"
func uninstallSystemDaemonDarwin(args []string) (ret error) {
if len(args) > 0 {
return errors.New("uninstall subcommand takes no arguments")
}
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
running := err == nil
if running {
out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
if err != nil {
fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
ret = err
}
out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
if err != nil {
fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
if ret == nil {
ret = err
}
}
}
err = os.Remove(sysPlist)
if os.IsNotExist(err) {
err = nil
if ret == nil {
ret = err
}
}
return ret
}
func installSystemDaemonDarwin(args []string) (err error) {
if len(args) > 0 {
return errors.New("install subcommand takes no arguments")
}
defer func() {
if err != nil && os.Getuid() != 0 {
err = fmt.Errorf("%w; try running tailscaled with sudo", err)
}
}()
// Copy ourselves to /usr/local/bin/tailscaled.
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
return err
}
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to find our own executable path: %w", err)
}
tmpBin := targetBin + ".tmp"
f, err := os.Create(tmpBin)
if err != nil {
return err
}
self, err := os.Open(exe)
if err != nil {
f.Close()
return err
}
_, err = io.Copy(f, self)
self.Close()
if err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Chmod(tmpBin, 0755); err != nil {
return err
}
if err := os.Rename(tmpBin, targetBin); err != nil {
return err
}
// Best effort:
uninstallSystemDaemonDarwin(nil)
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
}
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
}
return nil
}

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/pprof"
"os"
@@ -22,24 +21,20 @@ import (
"runtime"
"runtime/debug"
"strconv"
"sync"
"syscall"
"time"
"github.com/apenwarr/fixconsole"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/socks5"
"tailscale.com/paths"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/tstun"
)
// globalStateKey is the ipn.StateKey that tailscaled loads on
@@ -58,34 +53,19 @@ func defaultTunName() string {
return "tun"
case "windows":
return "Tailscale"
case "darwin":
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
// as a magic value that uses/creates any free number.
return "utun"
}
return "tailscale0"
}
var args struct {
cleanup bool
fake bool
debug string
tunname string
port uint16
statepath string
socketpath string
verbose int
socksAddr string // listen address for SOCKS5 server
}
var (
installSystemDaemon func([]string) error // non-nil on some platforms
uninstallSystemDaemon func([]string) error // non-nil on some platforms
)
var subCommands = map[string]*func([]string) error{
"install-system-daemon": &installSystemDaemon,
"uninstall-system-daemon": &uninstallSystemDaemon,
"debug": &debugModeFunc,
}
func main() {
@@ -100,31 +80,17 @@ func main() {
printVersion := false
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
if len(os.Args) > 1 {
sub := os.Args[1]
if fp, ok := subCommands[sub]; ok {
if *fp == nil {
log.SetFlags(0)
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
}
if err := (*fp)(os.Args[2:]); err != nil {
log.SetFlags(0)
log.Fatal(err)
}
return
}
}
if beWindowsSubprocess() {
return
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Fatalf("fixConsoleOutput: %v", err)
}
flag.Parse()
@@ -137,13 +103,11 @@ func main() {
os.Exit(0)
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && useTUN() {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
if args.statepath == "" {
log.Fatalf("--state is required")
}
if args.socketpath == "" && runtime.GOOS != "windows" {
log.SetFlags(0)
log.Fatalf("--socket is required")
}
@@ -165,16 +129,6 @@ func run() error {
pol.Shutdown(ctx)
}()
if isWindowsService() {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
if err := runWindowsService(pol); err != nil {
log.Printf("runservice: %v", err)
}
log.Printf("Service ended.")
return nil
}
var logf logger.Logf = log.Printf
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
logf = logger.RusagePrefixLog(logf)
@@ -186,90 +140,26 @@ func run() error {
return nil
}
if args.statepath == "" {
log.Fatalf("--state is required")
}
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()
go runDebugServer(debugMux, args.debug)
}
linkMon, err := monitor.New(logf)
if err != nil {
log.Fatalf("creating link monitor: %v", err)
}
pol.Logtail.SetLinkMonitor(linkMon)
var socksListener net.Listener
if args.socksAddr != "" {
var err error
socksListener, err = net.Listen("tcp", args.socksAddr)
if err != nil {
log.Fatalf("SOCKS5 listener: %v", err)
var e wgengine.Engine
if args.fake {
var impl wgengine.FakeImplFunc
if args.tunname == "userspace-networking" {
impl = netstack.Impl
}
}
conf := wgengine.Config{
ListenPort: args.port,
LinkMonitor: linkMon,
}
if useTUN() {
conf.TUNName = args.tunname
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl)
} else {
conf.TUN = tstun.NewFakeTUN()
conf.RouterGen = router.NewFake
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
}
e, err := wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
logf("wgengine.New: %v", err)
return err
}
var ns *netstack.Impl
if useNetstack() {
tunDev, magicConn := e.(wgengine.InternalsGetter).GetInternals()
ns, err = netstack.Create(logf, tunDev, e, magicConn)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
}
if socksListener != nil {
srv := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
}
if useNetstack() {
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
return ns.DialContextTCP(ctx, addr)
}
} else {
var mu sync.Mutex
var dns netstack.DNSMap
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
mu.Lock()
defer mu.Unlock()
dns = netstack.DNSMapFromNetworkMap(nm)
})
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := dns.Resolve(ctx, addr)
if err != nil {
return nil, err
}
var d net.Dialer
return d.DialContext(ctx, network, ipp.String())
}
}
go func() {
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
}()
}
e = wgengine.NewWatchdog(e)
ctx, cancel := context.WithCancel(context.Background())
@@ -329,6 +219,3 @@ func runDebugServer(mux *http.ServeMux, addr string) {
log.Fatal(err)
}
}
func useTUN() bool { return args.tunname != "userspace-networking" }
func useNetstack() bool { return !useTUN() }

View File

@@ -20,5 +20,22 @@ CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify
DeviceAllow=/dev/net/tun
DeviceAllow=/dev/null
DeviceAllow=/dev/random
DeviceAllow=/dev/urandom
DevicePolicy=strict
LockPersonality=true
MemoryDenyWriteExecute=true
PrivateTmp=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelTunables=true
ProtectSystem=strict
ReadWritePaths=/etc/
RestrictSUIDSGID=true
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target

View File

@@ -1,15 +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.
// +build !windows
package main // import "tailscale.com/cmd/tailscaled"
import "tailscale.com/logpolicy"
func isWindowsService() bool { return false }
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
func beWindowsSubprocess() bool { return false }

View File

@@ -1,228 +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.
package main // import "tailscale.com/cmd/tailscaled"
// TODO: check if administrator, like tswin does.
//
// TODO: try to load wintun.dll early at startup, before wireguard/tun
// does (which panics) and if we'd fail (e.g. due to access
// denied, even if administrator), use 'tasklist /m wintun.dll'
// to see if something else is currently using it and tell user.
//
// TODO: check if Tailscale service is already running, and fail early
// like tswin does.
//
// TODO: on failure, check if on a UNC drive and recommend copying it
// to C:\ to run it, like tswin does.
import (
"context"
"fmt"
"log"
"net"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/tempfork/wireguard-windows/firewall"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine"
)
const serviceName = "Tailscale"
func isWindowsService() bool {
v, err := svc.IsWindowsService()
if err != nil {
log.Fatalf("svc.IsWindowsService failed: %v", err)
}
return v
}
func runWindowsService(pol *logpolicy.Policy) error {
return svc.Run(serviceName, &ipnService{Policy: pol})
}
type ipnService struct {
Policy *logpolicy.Policy
}
// Called by Windows to execute the windows service.
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
changes <- svc.Status{State: svc.StartPending}
ctx, cancel := context.WithCancel(context.Background())
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
args := []string{"/subproc", service.Policy.PublicID.String()}
ipnserver.BabysitProc(ctx, args, log.Printf)
}()
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
for ctx.Err() == nil {
select {
case <-doneCh:
case cmd := <-r:
switch cmd.Cmd {
case svc.Stop:
cancel()
case svc.Interrogate:
changes <- cmd.CurrentStatus
}
}
}
changes <- svc.Status{State: svc.StopPending}
return false, windows.NO_ERROR
}
func beWindowsSubprocess() bool {
if beFirewallKillswitch() {
return true
}
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
return false
}
logid := os.Args[2]
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
go func() {
b := make([]byte, 16)
for {
_, err := os.Stdin.Read(b)
if err != nil {
log.Fatalf("stdin err (parent process died): %v", err)
}
}
}()
err := startIPNServer(context.Background(), logid)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}
return true
}
func beFirewallKillswitch() bool {
if len(os.Args) != 3 || os.Args[1] != "/firewall" {
return false
}
log.SetFlags(0)
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
go func() {
b := make([]byte, 16)
for {
_, err := os.Stdin.Read(b)
if err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
}
}()
guid, err := windows.GUIDFromString(os.Args[2])
if err != nil {
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
}
luid, err := winipcfg.LUIDFromGUID(&guid)
if err != nil {
log.Fatalf("no interface with GUID %q", guid)
}
noProtection := false
var dnsIPs []net.IP // unused in called code.
start := time.Now()
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
log.Printf("killswitch enabled, took %s", time.Since(start))
// Block until the monitor goroutine shuts us down.
select {}
}
func startIPNServer(ctx context.Context, logid string) error {
var logf logger.Logf = log.Printf
var eng wgengine.Engine
var err error
getEngine := func() (wgengine.Engine, error) {
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
TUNName: "Tailscale",
ListenPort: 41641,
})
if err != nil {
return nil, err
}
return wgengine.NewWatchdog(eng), nil
}
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
err = fmt.Errorf("pretending to be a service failure: %v", msg)
} else {
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
// restarting fixed. So try a few times.
for try := 1; try <= 5; try++ {
if try > 1 {
// Only sleep a bit. Don't do some massive backoff because
// the frontend GUI has a 30 second timeout on connecting to us,
// but even 5 seconds is too long for them to get any results.
// 5 tries * 1 second each seems fine.
time.Sleep(time.Second)
}
eng, err = getEngine()
if err != nil {
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
continue
}
if try > 1 {
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
}
break
}
}
if err != nil {
// Log the error, but don't fatalf. We want to
// propagate the error message to the UI frontend. So
// we continue and tell the ipnserver to return that
// in a Notify message.
logf("wgengine.NewUserspaceEngine: %v", err)
}
opts := ipnserver.Options{
Port: 41112,
SurviveDisconnects: false,
StatePath: args.statepath,
}
if err != nil {
// Return nicer errors to users, annotated with logids, which helps
// when they file bugs.
rawGetEngine := getEngine // raw == without verbose logid-containing error
getEngine = func() (wgengine.Engine, error) {
eng, err := rawGetEngine()
if err != nil {
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
}
return eng, nil
}
} else {
getEngine = ipnserver.FixedEngine(eng)
}
err = ipnserver.Run(ctx, logf, logid, getEngine, opts)
if err != nil {
logf("ipnserver.Run: %v", err)
}
return err
}

View File

@@ -32,9 +32,7 @@ import (
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
)
var (
@@ -98,13 +96,7 @@ func handleSSH(s ssh.Session) {
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) {
if !interfaces.IsTailscaleIP(ta.IP) {
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
s.Exit(1)
return

View File

@@ -18,13 +18,10 @@ import (
"time"
"golang.org/x/oauth2"
"tailscale.com/health"
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
"tailscale.com/types/wgkey"
)
@@ -71,9 +68,9 @@ type Status struct {
LoginFinished *empty.Message
Err string
URL string
Persist *persist.Persist // locally persisted configuration
NetMap *netmap.NetworkMap // server-pushed configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
Persist *Persist // locally persisted configuration
NetMap *NetworkMap // server-pushed configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
State State
}
@@ -117,8 +114,6 @@ type Client struct {
closed bool
newMapCh chan struct{} // readable when we must restart a map request
unregisterHealthWatch func()
mu sync.Mutex // mutex guards the following fields
statusFunc func(Status) // called to update Client status
@@ -174,14 +169,7 @@ func NewNoStart(opts Options) (*Client, error) {
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
return c, nil
}
func (c *Client) onHealthChange(key string, err error) {
c.logf("controlclient: restarting map request for %q health change to new state: %v", key, err)
c.cancelMapSafely()
}
// SetPaused controls whether HTTP activity should be paused.
@@ -225,7 +213,7 @@ func (c *Client) sendNewMapRequest() {
// If we're not already streaming a netmap, or if we're already stuck
// in a lite update, then tear down everything and start a new stream
// (which starts by sending a new map request)
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
if !c.inPollNetMap || c.inLiteMapUpdate {
c.mu.Unlock()
c.cancelMapSafely()
return
@@ -521,7 +509,7 @@ func (c *Client) mapRoutine() {
c.inPollNetMap = false
c.mu.Unlock()
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) {
c.mu.Lock()
select {
@@ -618,7 +606,7 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
c.sendNewMapRequest()
}
func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
c.mu.Lock()
state := c.state
loggedIn := c.loggedIn
@@ -630,7 +618,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ
c.logf("[v1] sendStatus: %s: %v", who, state)
var p *persist.Persist
var p *Persist
var fin *empty.Message
if state == StateAuthenticated {
fin = new(empty.Message)
@@ -710,7 +698,6 @@ func (c *Client) Shutdown() {
c.logf("client.Shutdown: inSendStatus=%v", inSendStatus)
if !closed {
c.unregisterHealthWatch()
close(c.quit)
c.cancelAuth()
<-c.authDone

View File

@@ -1,69 +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.
package controlclient
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"log"
"net/http"
"regexp"
"runtime"
"strconv"
"time"
)
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
zbuf := new(bytes.Buffer)
zw := gzip.NewWriter(zbuf)
zw.Write(scrubbedGoroutineDump())
zw.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
if err != nil {
log.Printf("dumpGoroutinesToURL: %v", err)
return
}
req.Header.Set("Content-Encoding", "gzip")
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
log.Printf("dumpGoroutinesToURL error: %v to %v (after %v)", err, targetURL, d)
} else {
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
}
}
var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`)
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
// values of arguments scrubbed out, lest it contain some private key material.
func scrubbedGoroutineDump() []byte {
buf := make([]byte, 1<<20)
buf = buf[:runtime.Stack(buf, true)]
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
if string(in) == "0x0" {
return in
}
if v, ok := saw[string(in)]; ok {
return v
}
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
if err != nil {
return []byte("??")
}
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
saw[string(in)] = v
return v
})
}

View File

@@ -1,11 +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.
package controlclient
import "testing"
func TestScrubbedGoroutineDump(t *testing.T) {
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
}

View File

@@ -4,6 +4,8 @@
package controlclient
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
import (
"bytes"
"context"
@@ -20,7 +22,6 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"sort"
@@ -33,26 +34,77 @@ import (
"golang.org/x/crypto/nacl/box"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/log/logheap"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
"tailscale.com/types/wgkey"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/monitor"
)
type Persist struct {
_ structs.Incomparable
// LegacyFrontendPrivateMachineKey is here temporarily
// (starting 2020-09-28) during migration of Windows users'
// machine keys from frontend storage to the backend. On the
// first LocalBackend.Start call, the backend will initialize
// the real (backend-owned) machine key from the frontend's
// provided value (if non-zero), picking a new random one if
// needed. This field should be considered read-only from GUI
// frontends. The real value should not be written back in
// this field, lest the frontend persist it to disk.
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
PrivateNodeKey wgkey.Private
OldPrivateNodeKey wgkey.Private // needed to request key rotation
Provider string
LoginName string
}
func (p *Persist) Equals(p2 *Persist) bool {
if p == nil && p2 == nil {
return true
}
if p == nil || p2 == nil {
return false
}
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.Provider == p2.Provider &&
p.LoginName == p2.LoginName
}
func (p *Persist) Pretty() string {
var mk, ok, nk wgkey.Key
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
mk = p.LegacyFrontendPrivateMachineKey.Public()
}
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
}
if !p.PrivateNodeKey.IsZero() {
nk = p.PrivateNodeKey.Public()
}
ss := func(k wgkey.Key) string {
if k.IsZero() {
return ""
}
return k.ShortString()
}
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
ss(mk), ss(ok), ss(nk), p.LoginName)
}
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
@@ -62,7 +114,6 @@ type Direct struct {
newDecompressor func() (Decompressor, error)
keepAlive bool
logf logger.Logf
linkMon *monitor.Mon // or nil
discoPubKey tailcfg.DiscoKey
machinePrivKey wgkey.Private
debugFlags []string
@@ -70,7 +121,7 @@ type Direct struct {
mu sync.Mutex // mutex guards the following fields
serverKey wgkey.Key
persist persist.Persist
persist Persist
authKey string
tryingNewKey wgkey.Private
expiry *time.Time
@@ -82,7 +133,7 @@ type Direct struct {
}
type Options struct {
Persist persist.Persist // initial persistent data
Persist Persist // initial persistent data
MachinePrivateKey wgkey.Private // the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
@@ -94,7 +145,6 @@ type Options struct {
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
DebugFlags []string // debug settings to send to control
LinkMonitor *monitor.Mon // optional link monitor
// KeepSharerAndUserSplit controls whether the client
// understands Node.Sharer. If false, the Sharer is mapped to the User.
@@ -131,18 +181,16 @@ func NewDirect(opts Options) (*Direct, error) {
httpc := opts.HTTPTestClient
if httpc == nil {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
}
dialer := netns.NewDialer()
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
httpc = &http.Client{Transport: tr}
}
@@ -159,7 +207,6 @@ func NewDirect(opts Options) (*Direct, error) {
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
linkMon: opts.LinkMonitor,
}
if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo())
@@ -182,25 +229,10 @@ func NewHostinfo() *tailcfg.Hostinfo {
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
Package: packageType(),
GoArch: runtime.GOARCH,
}
}
func packageType() string {
switch runtime.GOOS {
case "windows":
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
return "choco"
}
case "darwin":
// Using tailscaled or IPNExtension?
exe, _ := os.Executable()
return filepath.Base(exe)
}
return ""
}
// SetHostinfo clones the provided Hostinfo and remembers it for the
// next update. It reports whether the Hostinfo has changed.
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
@@ -239,7 +271,7 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
return true
}
func (c *Direct) GetPersist() persist.Persist {
func (c *Direct) GetPersist() Persist {
c.mu.Lock()
defer c.mu.Unlock()
return c.persist
@@ -262,7 +294,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
// immediately invalidated.
//if !c.persist.PrivateNodeKey.IsZero() {
//}
c.persist = persist.Persist{}
c.persist = Persist{}
return nil
}
@@ -494,7 +526,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
//
// maxPolls is how many network maps to download; common values are 1
// or -1 (to keep a long-poll query open to the server).
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
return c.sendMapRequest(ctx, maxPolls, cb)
}
@@ -506,7 +538,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
}
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
c.mu.Lock()
persist := c.persist
serverURL := c.serverURL
@@ -518,9 +550,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
everEndpoints := c.everEndpoints
c.mu.Unlock()
if persist.PrivateNodeKey.IsZero() {
return errors.New("privateNodeKey is zero")
}
if backendLogID == "" {
return errors.New("hostinfo: BackendLogID missing")
}
@@ -546,16 +575,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
}
var extraDebugFlags []string
if hostinfo != nil && c.linkMon != nil && ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy")
}
if len(extraDebugFlags) > 0 {
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
old := request.DebugFlags
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
}
if c.newDecompressor != nil {
request.Compress = "zstd"
@@ -677,10 +699,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return err
}
if pr := resp.PingRequest; pr != nil {
go answerPing(c.logf, c.httpc, pr)
}
if resp.KeepAlive {
vlogf("netmap: got keep-alive")
} else {
@@ -711,9 +729,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
if resp.Debug.LogHeapPprof {
go logheap.LogHeap(resp.Debug.LogHeapURL)
}
if resp.Debug.GoroutineDumpURL != "" {
go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL)
}
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
}
@@ -729,14 +744,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
resp.Peers = filtered
}
if Debug.StripEndpoints {
for _, p := range resp.Peers {
// We need at least one endpoint here for now else
// other code doesn't even create the discoEndpoint.
// TODO(bradfitz): fix that and then just nil this out.
p.Endpoints = []string{"127.9.9.9:456"}
}
}
if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf)
@@ -754,8 +761,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
localPort = c.localPort
c.mu.Unlock()
nm := &netmap.NetworkMap{
SelfNode: resp.Node,
nm := &NetworkMap{
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
@@ -784,10 +790,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
}
addUserProfile(nm.User)
magicDNSSuffix := nm.MagicDNSSuffix()
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
for _, peer := range resp.Peers {
peer.InitDisplayNames(magicDNSSuffix)
if !peer.Sharer.IsZero() {
if c.keepSharerAndUserSplit {
addUserProfile(peer.Sharer)
@@ -969,21 +972,19 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
}
func initDebug() debug {
use := os.Getenv("TS_DEBUG_USE_DISCO")
return debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
}
@@ -1064,24 +1065,6 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
}
}
sortNodes(newFull)
if mapRes.PeerSeenChange != nil {
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
}
now := time.Now()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {
n.LastSeen = &now
} else {
n.LastSeen = nil
}
}
}
}
mapRes.Peers = newFull
mapRes.PeersChanged = nil
mapRes.PeersRemoved = nil
@@ -1142,7 +1125,7 @@ func TrimWGConfig() opt.Bool {
// and will definitely not work for the routes provided.
//
// It should not return false positives.
func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool {
func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
if len(routes) == 0 {
// Nothing to route, so no need to warn.
return false
@@ -1156,21 +1139,8 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
return false
}
localIPs := map[netaddr.IP]bool{}
for _, addrs := range state.InterfaceIPs {
for _, pfx := range addrs {
localIPs[pfx.IP] = true
}
}
v4Routes, v6Routes := false, false
for _, r := range routes {
// It's possible to advertise a route to one of the local
// machine's local IPs. IP forwarding isn't required for this
// to work, so we shouldn't warn for such exports.
if r.IsSingleIP() && localIPs[r.IP] {
continue
}
if r.IP.Is4() {
v4Routes = true
} else {
@@ -1222,29 +1192,3 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
return false
}
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
if pr.URL == "" {
logf("invalid PingRequest with no URL")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "HEAD", pr.URL, nil)
if err != nil {
logf("http.NewRequestWithContext(%q): %v", pr.URL, err)
return
}
if pr.Log {
logf("answerPing: sending ping to %v ...", pr.URL)
}
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
logf("answerPing error: %v to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
logf("answerPing complete to %v (after %v)", pr.URL, d)
}
}

View File

@@ -0,0 +1,20 @@
// 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 -type Persist; DO NOT EDIT.
package controlclient
import ()
// Clone makes a deep copy of Persist.
// The result aliases no memory with the original.
func (src *Persist) Clone() *Persist {
if src == nil {
return nil
}
dst := new(Persist)
*dst = *src
return dst
}

View File

@@ -5,7 +5,6 @@
package controlclient
import (
"encoding/json"
"fmt"
"reflect"
"strings"
@@ -157,15 +156,3 @@ func TestNewDirect(t *testing.T) {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
}
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {
t.Fatal("no Hostinfo")
}
j, err := json.MarshalIndent(hi, " ", "")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}

View File

@@ -2,26 +2,29 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package netmap contains the netmap.NetworkMap type.
package netmap
package controlclient
import (
"encoding/json"
"fmt"
"net"
"reflect"
"strconv"
"strings"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/wgengine/filter"
)
type NetworkMap struct {
// Core networking
SelfNode *tailcfg.Node
NodeKey tailcfg.NodeKey
PrivateKey wgkey.Private
Expiry time.Time
@@ -60,16 +63,27 @@ type NetworkMap struct {
// TODO(crawshaw): Capabilities []tailcfg.Capability
}
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
// MagicDNS isn't necessarily in use).
//
// It will neither start nor end with a period.
// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none.
// If non-empty, it will neither start nor end with a period.
func (nm *NetworkMap) MagicDNSSuffix() string {
name := strings.Trim(nm.Name, ".")
if i := strings.Index(name, "."); i != -1 {
name = name[i+1:]
searchPathUsedAsDNSSuffix := func(suffix string) bool {
if dnsname.HasSuffix(nm.Name, suffix) {
return true
}
for _, p := range nm.Peers {
if dnsname.HasSuffix(p.Name, suffix) {
return true
}
}
return false
}
return name
for _, d := range nm.DNS.Domains {
if searchPathUsedAsDNSSuffix(d) {
return strings.Trim(d, ".")
}
}
return ""
}
func (nm *NetworkMap) String() string {
@@ -246,8 +260,110 @@ type WGConfigFlags int
const (
AllowSingleHosts WGConfigFlags = 1 << iota
AllowSubnetRoutes
AllowDefaultRoute
)
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
// This form is then recognize by magicsock's CreateEndpoint.
const EndpointDiscoSuffix = ".disco.tailscale:12345"
// WGCfg returns the NetworkMaps's Wireguard configuration.
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
cfg := &wgcfg.Config{
Name: "tailscale",
PrivateKey: wgcfg.PrivateKey(nm.PrivateKey),
Addresses: nm.Addresses,
ListenPort: nm.LocalPort,
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
}
for _, peer := range nm.Peers {
if Debug.OnlyDisco && peer.DiscoKey.IsZero() {
continue
}
if (flags&AllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
logf("wgcfg: %v skipping a single-host peer.", peer.Key.ShortString())
continue
}
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
PublicKey: wgcfg.Key(peer.Key),
})
cpeer := &cfg.Peers[len(cfg.Peers)-1]
if peer.KeepAlive {
cpeer.PersistentKeepalive = 25 // seconds
}
if !peer.DiscoKey.IsZero() {
if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], EndpointDiscoSuffix)); err != nil {
return nil, err
}
cpeer.Endpoints = fmt.Sprintf("%x.disco.tailscale:12345", peer.DiscoKey[:])
} else {
if err := appendEndpoint(cpeer, peer.DERP); err != nil {
return nil, err
}
for _, ep := range peer.Endpoints {
if err := appendEndpoint(cpeer, ep); err != nil {
return nil, err
}
}
}
for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Bits == 0 {
if (flags & AllowDefaultRoute) == 0 {
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
continue
}
} else if cidrIsSubnet(peer, allowedIP) {
if (flags & AllowSubnetRoutes) == 0 {
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
continue
}
}
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
}
}
return cfg, nil
}
// cidrIsSubnet reports whether cidr is a non-default-route subnet
// exported by node that is not one of its own self addresses.
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
if cidr.Bits == 0 {
return false
}
if !cidr.IsSingleIP() {
return true
}
for _, selfCIDR := range node.Addresses {
if cidr == selfCIDR {
return false
}
}
return true
}
func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
if epStr == "" {
return nil
}
_, port, err := net.SplitHostPort(epStr)
if err != nil {
return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
}
_, err = strconv.ParseUint(port, 10, 16)
if err != nil {
return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
}
if peer.Endpoints != "" {
peer.Endpoints += ","
}
peer.Endpoints += epStr
return nil
}
// eqStringsIgnoreNil reports whether a and b have the same length and
// contents, but ignore whether a or b are nil.
func eqStringsIgnoreNil(a, b []string) bool {

View File

@@ -2,10 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package netmap
package controlclient
import (
"encoding/hex"
"encoding/json"
"testing"
"inet.af/netaddr"
@@ -282,3 +283,15 @@ func TestConciseDiffFrom(t *testing.T) {
})
}
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {
t.Fatal("no Hostinfo")
}
j, err := json.MarshalIndent(hi, " ", "")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package persist
package controlclient
import (
"reflect"
@@ -11,15 +11,6 @@ import (
"tailscale.com/types/wgkey"
)
func fieldsOf(t reflect.Type) (fields []string) {
for i := 0; i < t.NumField(); i++ {
if name := t.Field(i).Name; name != "_" {
fields = append(fields, name)
}
}
return
}
func TestPersistEqual(t *testing.T) {
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {

View File

@@ -59,8 +59,7 @@ Login:
* server sends frameServerInfo
Steady state:
* server occasionally sends frameKeepAlive (or framePing)
* client responds to any framePing with a framePong
* server occasionally sends frameKeepAlive
* client sends frameSendPacket
* server then sends frameRecvPacket to recipient
*/
@@ -98,9 +97,6 @@ const (
// connection. (To be used for cluster load balancing
// purposes, when clients end up on a non-ideal node)
frameClosePeer = frameType(0x11) // 32B pub key of peer to close.
framePing = frameType(0x12) // 8 byte ping payload, to be echoed back in framePong
framePong = frameType(0x13) // 8 byte payload, the contents of the ping being replied to
)
var bin = binary.BigEndian

View File

@@ -21,14 +21,13 @@ import (
// Client is a DERP client.
type Client struct {
serverKey key.Public // of the DERP server; not a machine or node key
privateKey key.Private
publicKey key.Public // of privateKey
logf logger.Logf
nc Conn
br *bufio.Reader
meshKey string
canAckPings bool
serverKey key.Public // of the DERP server; not a machine or node key
privateKey key.Private
publicKey key.Public // of privateKey
logf logger.Logf
nc Conn
br *bufio.Reader
meshKey string
wmu sync.Mutex // hold while writing to bw
bw *bufio.Writer
@@ -49,9 +48,8 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
// clientOpt are the options passed to newClient.
type clientOpt struct {
MeshKey string
ServerPub key.Public
CanAckPings bool
MeshKey string
ServerPub key.Public
}
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
@@ -66,12 +64,6 @@ func ServerPublicKey(key key.Public) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
}
// CanAckPings returns a ClientOpt to set whether it advertises to the
// server that it's capable of acknowledging ping requests.
func CanAckPings(v bool) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
}
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
var opt clientOpt
for _, o := range opts {
@@ -85,14 +77,13 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
c := &Client{
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
nc: nc,
br: brw.Reader,
bw: brw.Writer,
meshKey: opt.MeshKey,
canAckPings: opt.CanAckPings,
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
nc: nc,
br: brw.Reader,
bw: brw.Writer,
meshKey: opt.MeshKey,
}
if opt.ServerPub.IsZero() {
if err := c.recvServerKey(); err != nil {
@@ -156,10 +147,6 @@ type clientInfo struct {
// connection list & forward packets. It's empty for regular
// users.
MeshKey string `json:"meshKey,omitempty"`
// CanAckPings is whether the client declares it's able to ack
// pings.
CanAckPings bool
}
func (c *Client) sendClientKey() error {
@@ -168,9 +155,8 @@ func (c *Client) sendClientKey() error {
return err
}
msg, err := json.Marshal(clientInfo{
Version: ProtocolVersion,
MeshKey: c.meshKey,
CanAckPings: c.canAckPings,
Version: ProtocolVersion,
MeshKey: c.meshKey,
})
if err != nil {
return err
@@ -252,18 +238,6 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error
func (c *Client) writeTimeoutFired() { c.nc.Close() }
func (c *Client) SendPong(data [8]byte) error {
c.wmu.Lock()
defer c.wmu.Unlock()
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
return err
}
if _, err := c.bw.Write(data[:]); err != nil {
return err
}
return c.bw.Flush()
}
// NotePreferred sends a packet that tells the server whether this
// client is the user's preferred server. This is only used in the
// server for stats.
@@ -345,12 +319,6 @@ type ServerInfoMessage struct{}
func (ServerInfoMessage) msg() {}
// PingMessage is a request from a client or server to reply to the
// other side with a PongMessage with the given payload.
type PingMessage [8]byte
func (PingMessage) msg() {}
// Recv reads a message from the DERP server.
//
// The returned message may alias memory owned by the Client; it
@@ -429,8 +397,8 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
// TODO: add the results of parseServerInfo to ServerInfoMessage if we ever need it.
return ServerInfoMessage{}, nil
case frameKeepAlive:
// A one-way keep-alive message that doesn't require an acknowledgement.
// This predated framePing/framePong.
// TODO: eventually we'll have server->client pings that
// require ack pongs.
continue
case framePeerGone:
if n < keyLen {
@@ -459,15 +427,6 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
copy(rp.Source[:], b[:keyLen])
rp.Data = b[keyLen:n]
return rp, nil
case framePing:
var pm PingMessage
if n < 8 {
c.logf("[unexpected] dropping short ping frame")
continue
}
copy(pm[:], b[:])
return pm, nil
}
}
}

View File

@@ -6,7 +6,6 @@ package derp
import (
"bufio"
"bytes"
"context"
crand "crypto/rand"
"crypto/x509"
@@ -792,63 +791,6 @@ func TestMetaCert(t *testing.T) {
}
}
type dummyNetConn struct {
net.Conn
}
func (dummyNetConn) SetReadDeadline(time.Time) error { return nil }
func TestClientRecv(t *testing.T) {
tests := []struct {
name string
input []byte
want interface{}
}{
{
name: "ping",
input: []byte{
byte(framePing), 0, 0, 0, 8,
1, 2, 3, 4, 5, 6, 7, 8,
},
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
nc: dummyNetConn{},
br: bufio.NewReader(bytes.NewReader(tt.input)),
logf: t.Logf,
}
got, err := c.Recv()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %#v; want %#v", got, tt.want)
}
})
}
}
func TestClientSendPong(t *testing.T) {
var buf bytes.Buffer
c := &Client{
bw: bufio.NewWriter(&buf),
}
if err := c.SendPong([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
t.Fatal(err)
}
want := []byte{
byte(framePong), 0, 0, 0, 8,
1, 2, 3, 4, 5, 6, 7, 8,
}
if !bytes.Equal(buf.Bytes(), want) {
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
}
}
func BenchmarkSendRecv(b *testing.B) {
for _, size := range []int{10, 100, 1000, 10000} {
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })

View File

@@ -63,7 +63,6 @@ type Client struct {
mu sync.Mutex
preferred bool
canAckPings bool
closed bool
netConn io.Closer
client *derp.Client
@@ -334,11 +333,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
}
}
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf,
derp.MeshKey(c.MeshKey),
derp.ServerPublicKey(serverPub),
derp.CanAckPings(c.canAckPings),
)
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
if err != nil {
return nil, 0, err
}
@@ -647,38 +642,6 @@ func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
return err
}
// SendPong sends a reply to a ping, with the ping's provided
// challenge/identifier data.
//
// Unlike other send methods, SendPong makes no attempt to connect or
// reconnect to the peer. It's best effort. If there's a connection
// problem, the server will choose to hang up on us if we're not
// replying.
func (c *Client) SendPong(data [8]byte) error {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return ErrClientClosed
}
if c.client == nil {
c.mu.Unlock()
return errors.New("not connected")
}
dc := c.client
c.mu.Unlock()
return dc.SendPong(data)
}
// SetCanAckPings sets whether this client will reply to ping requests from the server.
//
// This only affects future connections.
func (c *Client) SetCanAckPings(v bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.canAckPings = v
}
// NotePreferred notes whether this Client is the caller's preferred
// (home) DERP node. It's only used for stats.
func (c *Client) NotePreferred(v bool) {
@@ -746,19 +709,10 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
m, err = client.Recv()
if err != nil {
c.closeForReconnect(client)
if c.isClosed() {
err = ErrClientClosed
}
}
return m, connGen, err
}
func (c *Client) isClosed() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.closed
}
// Close closes the client. It will not automatically reconnect after
// being closed.
func (c *Client) Close() error {

View File

@@ -5,32 +5,20 @@
package derphttp
import (
"context"
"sync"
"time"
"tailscale.com/derp"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
// RunWatchConnectionLoop loops until ctx is done, sending WatchConnectionChanges and subscribing to
// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to
// connection changes.
//
// If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns.
//
// Otherwise, the add and remove funcs are called as clients come & go.
//
// infoLogf, if non-nil, is the logger to write periodic status
// updates about how many peers are on the server. Error log output is
// set to the c's logger, regardless of infoLogf's value.
//
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
// be closed, and c itself needs to be closed.
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
if infoLogf == nil {
infoLogf = logger.Discard
}
func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) {
logf := c.logf
const retryInterval = 5 * time.Second
const statusInterval = 10 * time.Second
@@ -57,7 +45,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
if loggedConnected {
return
}
infoLogf("connected; %d peers", len(present))
logf("connected; %d peers", len(present))
loggedConnected = true
}
@@ -91,21 +79,12 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
}
sleep := func(d time.Duration) {
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-t.C:
}
}
for ctx.Err() == nil {
for {
err := c.WatchConnectionChanges()
if err != nil {
clear()
logf("WatchConnectionChanges: %v", err)
sleep(retryInterval)
time.Sleep(retryInterval)
continue
}
@@ -118,7 +97,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
if err != nil {
clear()
logf("Recv: %v", err)
sleep(retryInterval)
time.Sleep(retryInterval)
break
}
if connGen != lastConnGen {
@@ -135,8 +114,9 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
lastStatus = now
infoLogf("%d peers", len(present))
logf("%d peers", len(present))
}
}
}
}

View File

@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
case TypePong:
return parsePong(ver, p)
case TypeCallMeMaybe:
return parseCallMeMaybe(ver, p)
return CallMeMaybe{}, nil
default:
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
}
@@ -122,57 +122,13 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
//
// The recipient may choose to not open a path back, if it's already
// happy with its path. But usually it will.
type CallMeMaybe struct {
// MyNumber is what the peer believes its endpoints are.
//
// Prior to Tailscale 1.4, the endpoints were exchanged purely
// between nodes and the control server.
//
// Starting with Tailscale 1.4, clients advertise their endpoints.
// Older clients won't use this, but newer clients should
// use any endpoints in here that aren't included from control.
//
// Control might have sent stale endpoints if the client was idle
// before contacting us. In that case, the client likely did a STUN
// request immediately before sending the CallMeMaybe to recreate
// their NAT port mapping, and that new good endpoint is included
// in this field, but might not yet be in control's endpoints.
// (And in the future, control will stop distributing endpoints
// when clients are suitably new.)
MyNumber []netaddr.IPPort
}
type CallMeMaybe struct{}
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
for _, ipp := range m.MyNumber {
a := ipp.IP.As16()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port)
p = p[epLength:]
}
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
return ret
}
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
m = new(CallMeMaybe)
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
return m, nil
}
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
for len(p) > 0 {
var a [16]byte
copy(a[:], p)
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
IP: netaddr.IPFrom16(a),
Port: binary.BigEndian.Uint16(p[16:18]),
})
p = p[epLength:]
}
return m, nil
}
// Pong is a response a Ping.
//
// It includes the sender's source IP + port, so it's effectively a
@@ -215,7 +171,7 @@ func MessageSummary(m Message) string {
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case *CallMeMaybe:
case CallMeMaybe:
return "call-me-maybe"
default:
return fmt.Sprintf("%#v", m)

View File

@@ -44,19 +44,9 @@ func TestMarshalAndParse(t *testing.T) {
},
{
name: "call_me_maybe",
m: &CallMeMaybe{},
m: CallMeMaybe{},
want: "03 00",
},
{
name: "call_me_maybe_endpoints",
m: &CallMeMaybe{
MyNumber: []netaddr.IPPort{
netaddr.MustParseIPPort("1.2.3.4:567"),
netaddr.MustParseIPPort("[2001::3456]:789"),
},
},
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

19
go.mod
View File

@@ -1,6 +1,6 @@
module tailscale.com
go 1.16
go 1.15
require (
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
@@ -15,31 +15,30 @@ require (
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/go-cmp v0.5.4
github.com/goreleaser/nfpm v1.1.10
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
github.com/klauspost/compress v1.10.10
github.com/kr/pty v1.1.8
github.com/mdlayher/netlink v1.3.2
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1
github.com/mdlayher/netlink v1.2.0
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
github.com/miekg/dns v1.1.30
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/peterbourgon/ff/v2 v2.0.0
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
golang.org/x/net v0.0.0-20201216054612-986b41b23924
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6
honnef.co/go/tools v0.1.0
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
inet.af/peercred v0.0.0-20210302202138-56e694897155
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d
rsc.io/goversion v1.2.0
)

77
go.sum
View File

@@ -68,8 +68,6 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
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/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -123,6 +121,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -141,7 +140,9 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -178,16 +179,12 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -201,10 +198,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1 h1:zc0R6cOw98cMengLA0fvU55mqbnN7sd/tBMLzSejp+M=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
@@ -213,20 +210,13 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.3.2 h1:fMZOU2/M7PRMzGM3br5l1N2fu6bPSHtRytmQ338a9iA=
github.com/mdlayher/netlink v1.3.2/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
@@ -261,6 +251,7 @@ github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqB
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -295,8 +286,14 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM=
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e h1:ZXbXfVJOhSq4/Gt7TnqwXBPCctzYXkWXo3oQS7LZ40I=
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98 h1:khwYPK1eT+4pmEFyCjpf6Br/0JWjdVT3uQ+ILFJPTRo=
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa145kVflAFkVcTr/zwUzBO4SqfSS6xhbcMv8=
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
@@ -315,12 +312,15 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8=
go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
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 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
@@ -334,8 +334,8 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -382,12 +382,10 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -440,17 +438,9 @@ golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b h1:kHlr0tATeLRMEiZJu5CknOw/E8V6h69sXXQFGoPtjcc=
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
@@ -548,6 +538,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -561,14 +552,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8=
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg=
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4=
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa h1:6qseJO2iNDHl+MLL2BkO5oURJR4A9pLmRz11Yf7KdGM=
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa/go.mod h1:VZeNdG7cRIUqKl9DWoFX86AHyfYwdb4RextAw1CAEO4=
inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE=
inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=

View File

@@ -1,71 +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.
// Package health is a registry for other packages to report & check
// overall health status of the node.
package health
import (
"sync"
)
var (
mu sync.Mutex
m = map[string]error{} // error key => err (or nil for no error)
watchers = map[*watchHandle]func(string, error){} // opt func to run if error state changes
)
type watchHandle byte
// RegisterWatcher adds a function that will be called if an
// error changes state either to unhealthy or from unhealthy. It is
// not called on transition from unknown to healthy. It must be non-nil
// and is run in its own goroutine. The returned func unregisters it.
func RegisterWatcher(cb func(errKey string, err error)) (unregister func()) {
mu.Lock()
defer mu.Unlock()
handle := new(watchHandle)
watchers[handle] = cb
return func() {
mu.Lock()
defer mu.Unlock()
delete(watchers, handle)
}
}
// SetRouter sets the state of the wgengine/router.Router.
func SetRouterHealth(err error) { set("router", err) }
// RouterHealth returns the wgengine/router.Router error state.
func RouterHealth() error { return get("router") }
func get(key string) error {
mu.Lock()
defer mu.Unlock()
return m[key]
}
func set(key string, err error) {
mu.Lock()
defer mu.Unlock()
old, ok := m[key]
if !ok && err == nil {
// Initial happy path.
m[key] = nil
return
}
if ok && (old == nil) == (err == nil) {
// No change in overall error status (nil-vs-not), so
// don't run callbacks, but exact error might've
// changed, so note it.
if err != nil {
m[key] = err
}
return
}
m[key] = err
for _, cb := range watchers {
go cb(key, err)
}
}

View File

@@ -8,10 +8,10 @@ import (
"bytes"
"testing"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/wgcfg"
)
func TestDeepPrint(t *testing.T) {

View File

@@ -9,11 +9,12 @@ import (
"time"
"golang.org/x/oauth2"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/netmap"
"tailscale.com/types/structs"
"tailscale.com/wgengine"
)
type State int
@@ -45,10 +46,10 @@ func (s State) String() string {
// EngineStatus contains WireGuard engine stats.
type EngineStatus struct {
RBytes, WBytes int64
RBytes, WBytes wgengine.ByteCount
NumLive int
LiveDERPs int // number of active DERP connections
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
}
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
@@ -58,16 +59,16 @@ type EngineStatus struct {
// They are JSON-encoded on the wire, despite the lack of struct tags.
type Notify struct {
_ structs.Incomparable
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *netmap.NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
Status *ipnstate.Status // full status
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *controlclient.NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
Status *ipnstate.Status // full status
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
PingResult *ipnstate.PingResult
// LocalTCPPort, if non-nil, informs the UI frontend which

View File

@@ -9,8 +9,8 @@ import (
"time"
"golang.org/x/oauth2"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/netmap"
)
type FakeBackend struct {
@@ -54,7 +54,7 @@ func (b *FakeBackend) login() {
b.newState(NeedsMachineAuth)
b.newState(Stopped)
// TODO(apenwarr): Fill in a more interesting netmap here.
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
b.newState(Starting)
// TODO(apenwarr): Fill in a more interesting status.
b.notify(Notify{Engine: &EngineStatus{}})
@@ -92,7 +92,7 @@ func (b *FakeBackend) RequestStatus() {
}
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
}
func (b *FakeBackend) Ping(ip string) {

View File

@@ -10,8 +10,8 @@ import (
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
)
type Handle struct {
@@ -22,7 +22,7 @@ type Handle struct {
// Mutex protects everything below
mu sync.Mutex
netmapCache *netmap.NetworkMap
netmapCache *controlclient.NetworkMap
engineStatusCache EngineStatus
stateCache State
prefsCache *Prefs
@@ -129,7 +129,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
return []netaddr.IPPrefix{}
}
func (h *Handle) NetMap() *netmap.NetworkMap {
func (h *Handle) NetMap() *controlclient.NetworkMap {
h.mu.Lock()
defer h.mu.Unlock()

View File

@@ -1,267 +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.
package ipnlocal
import (
"reflect"
"testing"
"inet.af/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/wgcfg"
)
func TestNetworkMapCompare(t *testing.T) {
prefix1, err := netaddr.ParseIPPrefix("192.168.0.0/24")
if err != nil {
t.Fatal(err)
}
node1 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix1}}
prefix2, err := netaddr.ParseIPPrefix("10.0.0.0/8")
if err != nil {
t.Fatal(err)
}
node2 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix2}}
tests := []struct {
name string
a, b *netmap.NetworkMap
want bool
}{
{
"both nil",
nil,
nil,
true,
},
{
"b nil",
&netmap.NetworkMap{},
nil,
false,
},
{
"a nil",
nil,
&netmap.NetworkMap{},
false,
},
{
"both default",
&netmap.NetworkMap{},
&netmap.NetworkMap{},
true,
},
{
"names identical",
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map1"},
true,
},
{
"names differ",
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map2"},
false,
},
{
"Peers identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
true,
},
{
"Peer list length",
// length of Peers list differs
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
false,
},
{
"Node names identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
true,
},
{
"Node names differ",
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
false,
},
{
"Node lists identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
true,
},
{
"Node lists differ",
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
false,
},
{
"Node Users differ",
// User field is not checked.
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
true,
},
}
for _, tt := range tests {
got := dnsMapsEqual(tt.a, tt.b)
if got != tt.want {
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
}
}
}
func TestShrinkDefaultRoute(t *testing.T) {
tests := []struct {
route string
in []string
out []string
}{
{
route: "0.0.0.0/0",
in: []string{"1.2.3.4", "25.0.0.1"},
out: []string{
"10.0.0.1",
"10.255.255.255",
"192.168.0.1",
"192.168.255.255",
"172.16.0.1",
"172.31.255.255",
"100.101.102.103",
// Some random IPv6 stuff that shouldn't be in a v4
// default route.
"fe80::",
"2601::1",
},
},
{
route: "::/0",
in: []string{"::1", "2601::1"},
out: []string{
"fe80::1",
tsaddr.TailscaleULARange().IP.String(),
},
},
}
for _, test := range tests {
def := netaddr.MustParseIPPrefix(test.route)
got, err := shrinkDefaultRoute(def)
if err != nil {
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
}
for _, ip := range test.in {
if !got.Contains(netaddr.MustParseIP(ip)) {
t.Errorf("shrink(%q).Contains(%v) = false, want true", test.route, ip)
}
}
for _, ip := range test.out {
if got.Contains(netaddr.MustParseIP(ip)) {
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
}
}
}
}
func TestPeerRoutes(t *testing.T) {
pp := netaddr.MustParseIPPrefix
tests := []struct {
name string
peers []wgcfg.Peer
want []netaddr.IPPrefix
}{
{
name: "small_v4",
peers: []wgcfg.Peer{
{
AllowedIPs: []netaddr.IPPrefix{
pp("100.101.102.103/32"),
},
},
},
want: []netaddr.IPPrefix{
pp("100.101.102.103/32"),
},
},
{
name: "big_v4",
peers: []wgcfg.Peer{
{
AllowedIPs: []netaddr.IPPrefix{
pp("100.101.102.103/32"),
pp("100.101.102.104/32"),
pp("100.101.102.105/32"),
},
},
},
want: []netaddr.IPPrefix{
pp("100.64.0.0/10"),
},
},
{
name: "has_1_v6",
peers: []wgcfg.Peer{
{
AllowedIPs: []netaddr.IPPrefix{
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
},
},
},
want: []netaddr.IPPrefix{
pp("fd7a:115c:a1e0::/48"),
},
},
{
name: "has_2_v6",
peers: []wgcfg.Peer{
{
AllowedIPs: []netaddr.IPPrefix{
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
},
},
},
want: []netaddr.IPPrefix{
pp("fd7a:115c:a1e0::/48"),
},
},
{
name: "big_v4_big_v6",
peers: []wgcfg.Peer{
{
AllowedIPs: []netaddr.IPPrefix{
pp("100.101.102.103/32"),
pp("100.101.102.104/32"),
pp("100.101.102.105/32"),
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
},
},
},
want: []netaddr.IPPrefix{
pp("fd7a:115c:a1e0::/48"),
pp("100.64.0.0/10"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := peerRoutes(tt.peers, 2)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got = %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,49 @@
// 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.
// +build linux
package ipnserver
import (
"net"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func isReadonlyConn(c net.Conn, logf logger.Logf) (ro bool) {
ro = true // conservative default for naked returns below
uc, ok := c.(*net.UnixConn)
if !ok {
logf("unexpected connection type %T", c)
return
}
raw, err := uc.SyscallConn()
if err != nil {
logf("SyscallConn: %v", err)
return
}
var cred *unix.Ucred
cerr := raw.Control(func(fd uintptr) {
cred, err = unix.GetsockoptUcred(int(fd),
unix.SOL_SOCKET,
unix.SO_PEERCRED)
})
if cerr != nil {
logf("raw.Control: %v", err)
return
}
if err != nil {
logf("raw.Control: %v", err)
return
}
if cred.Uid == 0 {
// root is not read-only.
return false
}
logf("non-root connection from %v (read-only)", cred.Uid)
return true
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux
package ipnserver
import (
"net"
"tailscale.com/types/logger"
)
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
// Windows doesn't need/use this mechanism, at least yet. It
// has a different last-user-wins auth model.
// And on Darwin, we're not using it yet, as the Darwin
// tailscaled port isn't yet done, and unix.Ucred and
// unix.GetsockoptUcred aren't in x/sys/unix.
// TODO(bradfitz): OpenBSD and FreeBSD should implement this too.
// But their x/sys/unix package is different than Linux, so
// I didn't include it for now.
return false
}

View File

@@ -19,20 +19,14 @@ import (
"os/signal"
"os/user"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"go4.org/mem"
"inet.af/netaddr"
"inet.af/peercred"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
@@ -97,7 +91,7 @@ type Options struct {
// server is an IPN backend and its set of 0 or more active connections
// talking to an IPN backend.
type server struct {
b *ipnlocal.LocalBackend
b *ipn.LocalBackend
logf logger.Logf
// resetOnZero is whether to call bs.Reset on transition from
// 1->0 connections. That is, this is whether the backend is
@@ -117,34 +111,21 @@ type server struct {
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
}
// connIdentity represents the owner of a localhost TCP or unix socket connection.
// connIdentity represents the owner of a localhost TCP connection.
type connIdentity struct {
Conn net.Conn
NotWindows bool // runtime.GOOS != "windows"
// Fields used when NotWindows:
IsUnixSock bool // Conn is a *net.UnixConn
Creds *peercred.Creds // or nil
// Used on Windows:
// TODO(bradfitz): merge these into the peercreds package and
// use that for all.
Pid int
UserID string
User *user.User
Unknown bool
Pid int
UserID string
User *user.User
}
// getConnIdentity returns the localhost TCP connection's identity information
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
// and a ConnIdentity with NotWindows set true. It's only an error if we expected
// and a ConnIdentity with Unknown set true. It's only an error if we expected
// to be able to map it and couldn't.
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
ci = connIdentity{Conn: c}
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
ci.NotWindows = true
_, ci.IsUnixSock = c.(*net.UnixConn)
ci.Creds, _ = peercred.Get(c)
return ci, nil
return connIdentity{Unknown: true}, nil
}
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
if err != nil {
@@ -235,22 +216,13 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
}
}
// bufferHasHTTPRequest reports whether br looks like it has an HTTP
// request in it, without reading any bytes from it.
func bufferHasHTTPRequest(br *bufio.Reader) bool {
peek, _ := br.Peek(br.Buffered())
return mem.HasPrefix(mem.B(peek), mem.S("GET ")) ||
mem.HasPrefix(mem.B(peek), mem.S("POST ")) ||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
}
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
// First see if it's an HTTP request.
br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second))
br.Peek(4)
peek, _ := br.Peek(4)
c.SetReadDeadline(time.Time{})
isHTTPReq := bufferHasHTTPRequest(br)
isHTTPReq := string(peek) == "GET "
ci, err := s.addConn(c, isHTTPReq)
if err != nil {
@@ -277,7 +249,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
s.b.SetCurrentUserID(ci.UserID)
if isHTTPReq {
httpServer := &http.Server{
httpServer := http.Server{
// Localhost connections are cheap; so only do
// keep-alives for a short period of time, as these
// active connections lock the server into only serving
@@ -296,7 +268,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
defer s.removeAndCloseConn(c)
logf("[v1] incoming control connection")
if isReadonlyConn(ci, logf) {
if isReadonlyConn(c, logf) {
ctx = ipn.ReadonlyContextOf(ctx)
}
@@ -322,82 +294,6 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
}
}
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
if runtime.GOOS == "windows" {
// Windows doesn't need/use this mechanism, at least yet. It
// has a different last-user-wins auth model.
return false
}
const ro = true
const rw = false
if !safesocket.PlatformUsesPeerCreds() {
return rw
}
creds := ci.Creds
if creds == nil {
logf("connection from unknown peer; read-only")
return ro
}
uid, ok := creds.UserID()
if !ok {
logf("connection from peer with unknown userid; read-only")
return ro
}
if uid == "0" {
logf("connection from userid %v; root has access", uid)
return rw
}
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
return rw
}
var adminGroupID string
switch runtime.GOOS {
case "darwin":
adminGroupID = darwinAdminGroupID()
default:
logf("connection from userid %v; read-only", uid)
return ro
}
if adminGroupID == "" {
logf("connection from userid %v; no system admin group found, read-only", uid)
return ro
}
u, err := user.LookupId(uid)
if err != nil {
logf("connection from userid %v; failed to look up user; read-only", uid)
return ro
}
gids, err := u.GroupIds()
if err != nil {
logf("connection from userid %v; failed to look up groups; read-only", uid)
return ro
}
for _, gid := range gids {
if gid == adminGroupID {
logf("connection from userid %v; is local admin, has access", uid)
return rw
}
}
logf("connection from userid %v; read-only", uid)
return ro
}
var darwinAdminGroupIDCache atomic.Value // of string
func darwinAdminGroupID() string {
s, _ := darwinAdminGroupIDCache.Load().(string)
if s != "" {
return s
}
g, err := user.LookupGroup("admin")
if err != nil {
return ""
}
darwinAdminGroupIDCache.Store(g.Gid)
return g.Gid
}
// inUseOtherUserError is the error type for when the server is in use
// by a different local user.
type inUseOtherUserError struct{ error }
@@ -430,25 +326,6 @@ func (s *server) checkConnIdentityLocked(ci connIdentity) error {
return nil
}
// localAPIPermissions returns the permissions for the given identity accessing
// the Tailscale local daemon API.
//
// s.mu must not be held.
func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
if runtime.GOOS == "windows" {
s.mu.Lock()
defer s.mu.Unlock()
if s.checkConnIdentityLocked(ci) == nil {
return true, true
}
return false, false
}
if ci.IsUnixSock {
return true, !isReadonlyConn(ci, logger.Discard)
}
return false, false
}
// registerDisconnectSub adds ch as a subscribe to connection disconnect
// events. If add is false, the subscriber is removed.
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
@@ -565,7 +442,7 @@ func (s *server) setServerModeUserLocked() {
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
return
}
if ci.NotWindows {
if ci.Unknown {
return
}
if ci.User != nil {
@@ -626,6 +503,41 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}()
logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
if err := ctx.Err(); err != nil {
return err
}
}
var store ipn.StateStore
if opts.StatePath != "" {
store, err = ipn.NewFileStore(opts.StatePath)
@@ -654,83 +566,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
store = &ipn.MemoryStore{}
}
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
// Issue 1187: on Windows, in unattended mode,
// sometimes we try 5 times and fail to create the
// engine before the system's ready. Hack until the
// bug if fixed properly: if we're running in
// unattended mode on Windows, keep trying forever,
// waiting for the machine to be ready (networking to
// come up?) and then dial our own safesocket TCP
// listener to wake up the usual mechanism that lets
// us surface getEngine errors to UI clients. (We
// don't want to just call getEngine in a loop without
// the listener.Accept, as we do want to handle client
// connections so we can tell them about errors)
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
logf("ipnserver: in unattended mode, waiting for engine availability")
getEngine = getEngineUntilItWorksWrapper(getEngine)
// Wait for it to be ready.
go func() {
defer bootRaceWaitForEngineCancel()
t0 := time.Now()
for {
time.Sleep(10 * time.Second)
if _, err := getEngine(); err != nil {
logf("ipnserver: unattended mode engine load: %v", err)
continue
}
c, err := net.Dial("tcp", listen.Addr().String())
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
if err == nil {
c.Close()
}
break
}
}()
} else {
bootRaceWaitForEngineCancel()
}
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
<-bootRaceWaitForEngine.Done()
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
if err := ctx.Err(); err != nil {
return err
}
}
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
@@ -920,27 +756,6 @@ 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) {
var mu sync.Mutex
var engGood wgengine.Engine
return func() (wgengine.Engine, error) {
mu.Lock()
defer mu.Unlock()
if engGood != nil {
return engGood, nil
}
e, err := getEngine()
if err != nil {
return nil, err
}
engGood = e
return e, nil
}
}
type dummyAddr string
type oneConnListener struct {
conn net.Conn
@@ -982,15 +797,8 @@ func (psc *protoSwitchConn) Close() error {
}
func (s *server) localhostHandler(ci connIdentity) http.Handler {
lah := localapi.NewHandler(s.b)
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/localapi/") {
lah.ServeHTTP(w, r)
return
}
if ci.NotWindows {
if ci.Unknown {
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
return
}
@@ -998,7 +806,7 @@ func (s *server) localhostHandler(ci connIdentity) http.Handler {
})
}
func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) {
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?

View File

@@ -56,7 +56,7 @@ func TestRunMultipleAccepts(t *testing.T) {
}
}
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -21,22 +21,14 @@ import (
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/dnsname"
)
// Status represents the entire state of the IPN network.
type Status struct {
BackendState string
AuthURL string // current URL provided by control to authorize client
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
// in the network such as "userfoo.tailscale.net".
// There are no surrounding dots.
// MagicDNSSuffix should be populated regardless of whether a domain
// has MagicDNS enabled.
MagicDNSSuffix string
BackendState string
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
Peer map[key.Public]*PeerStatus
User map[tailcfg.UserID]tailcfg.UserProfile
@@ -51,12 +43,6 @@ func (s *Status) Peers() []key.Public {
return kk
}
type PeerStatusLite struct {
TxBytes, RxBytes int64
LastHandshake time.Time
NodeKey tailcfg.NodeKey
}
type PeerStatus struct {
PublicKey key.Public
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
@@ -78,7 +64,6 @@ type PeerStatus struct {
LastSeen time.Time // last seen to tailcontrol
LastHandshake time.Time // with local wireguard
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
@@ -99,6 +84,14 @@ type PeerStatus struct {
InEngine bool
}
// SimpleHostName returns a potentially simplified version of ps.HostName for display purposes.
func (ps *PeerStatus) SimpleHostName() string {
n := ps.HostName
n = strings.TrimSuffix(n, ".local")
n = strings.TrimSuffix(n, ".localdomain")
return n
}
type StatusBuilder struct {
mu sync.Mutex
locked bool
@@ -111,12 +104,6 @@ func (sb *StatusBuilder) SetBackendState(v string) {
sb.st.BackendState = v
}
func (sb *StatusBuilder) SetAuthURL(v string) {
sb.mu.Lock()
defer sb.mu.Unlock()
sb.st.AuthURL = v
}
func (sb *StatusBuilder) SetMagicDNSSuffix(v string) {
sb.mu.Lock()
defer sb.mu.Unlock()
@@ -244,9 +231,6 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if st.KeepAlive {
e.KeepAlive = true
}
if st.ExitNode {
e.ExitNode = true
}
if st.ShareeNode {
e.ShareeNode = true
}
@@ -290,22 +274,13 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
f("<table>\n<thead>\n")
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
f("</thead>\n<tbody>\n")
now := time.Now()
var peers []*PeerStatus
for _, peer := range st.Peers() {
ps := st.Peer[peer]
if ps.ShareeNode {
continue
}
peers = append(peers, ps)
}
SortPeers(peers)
for _, ps := range peers {
var actAgo string
if !ps.LastWrite.IsZero() {
ago := now.Sub(ps.LastWrite)
@@ -321,41 +296,40 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
owner = owner[:i]
}
}
hostName := dnsname.SanitizeHostname(ps.HostName)
dnsName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
hostName = ""
}
var hostNameHTML string
if hostName != "" {
hostNameHTML = "<br>" + html.EscapeString(hostName)
}
f("<tr><td>%s</td><td class=acenter>%s</td>"+
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
ps.PublicKey.ShortString(),
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
peer.ShortString(),
html.EscapeString(ps.SimpleHostName()),
osEmoji(ps.OS),
html.EscapeString(dnsName),
hostNameHTML,
ps.TailAddr,
html.EscapeString(owner),
ps.RxBytes,
ps.TxBytes,
actAgo,
)
f("<td>")
f("<td class=\"aright\">")
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
if active {
if ps.Relay != "" && ps.CurAddr == "" {
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
} else if ps.CurAddr != "" {
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
relay := ps.Relay
if relay != "" {
if active && ps.CurAddr == "" {
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
} else {
f("derp-%v<br>", html.EscapeString(relay))
}
}
match := false
for _, addr := range ps.Addrs {
if addr == ps.CurAddr {
match = true
f("🔗 <b>%s</b><br>", addr)
} else {
f("%s<br>", addr)
}
}
if ps.CurAddr != "" && !match {
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
}
f("</td>") // end Addrs
f("</tr>\n")
@@ -401,17 +375,3 @@ type PingResult struct {
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
}
func SortPeers(peers []*PeerStatus) {
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
}
func sortKey(ps *PeerStatus) string {
if ps.DNSName != "" {
return ps.DNSName
}
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnlocal
package ipn
import (
"bytes"
@@ -15,11 +15,11 @@ import (
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/internal/deepprint"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
"tailscale.com/net/interfaces"
@@ -29,8 +29,6 @@ import (
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/util/systemd"
"tailscale.com/version"
@@ -39,8 +37,6 @@ import (
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/tsdns"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgcfg/nmcfg"
)
var controlDebugFlags = getControlDebugFlags()
@@ -64,40 +60,38 @@ func getControlDebugFlags() []string {
// state machine generates events back out to zero or more components.
type LocalBackend struct {
// Elements that are thread-safe or constant after construction.
ctx context.Context // canceled by Close
ctxCancel context.CancelFunc // cancels ctx
logf logger.Logf // general logging
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store ipn.StateStore
backendLogID string
unregisterLinkMon func()
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once // guards starting readPoller
gotPortPollRes chan struct{} // closed upon first readPoller result
serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error)
ctx context.Context // canceled by Close
ctxCancel context.CancelFunc // cancels ctx
logf logger.Logf // general logging
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store StateStore
backendLogID string
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once // guards starting readPoller
gotPortPollRes chan struct{} // closed upon first readPoller result
serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error)
filterHash string
// The mutex protects the following elements.
mu sync.Mutex
notify func(ipn.Notify)
notify func(Notify)
c *controlclient.Client
stateKey ipn.StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *ipn.Prefs
stateKey StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *Prefs
inServerMode bool
machinePrivKey wgkey.Private
state ipn.State
state State
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *netmap.NetworkMap
nodeByAddr map[netaddr.IP]*tailcfg.Node
netMap *controlclient.NetworkMap
activeLogin string // last logged LoginName from netMap
engineStatus ipn.EngineStatus
engineStatus EngineStatus
endpoints []string
blocked bool
authURL string
@@ -112,13 +106,13 @@ type LocalBackend struct {
// NewLocalBackend returns a new LocalBackend that is ready to run,
// but is not actually running.
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengine.Engine) (*LocalBackend, error) {
if e == nil {
panic("ipn.NewLocalBackend: wgengine must not be nil")
}
// Default filter blocks everything and logs nothing, until Start() is called.
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
// Default filter blocks everything, until Start() is called.
e.SetFilter(filter.NewAllowNone(logf))
ctx, cancel := context.WithCancel(context.Background())
portpoll, err := portlist.NewPoller()
@@ -135,23 +129,18 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
e: e,
store: store,
backendLogID: logid,
state: ipn.NoState,
state: NoState,
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
}
e.SetLinkChangeCallback(b.linkChange)
b.statusChanged = sync.NewCond(&b.statusLock)
linkMon := e.GetLinkMonitor()
// Call our linkChange code once with the current state, and
// then also whenever it changes:
b.linkChange(false, linkMon.InterfaceState())
b.unregisterLinkMon = linkMon.RegisterChangeCallback(b.linkChange)
return b, nil
}
// linkChange is our link monitor callback, called whenever the network changes.
// major is whether ifst is different than earlier.
// linkChange is called (in a new goroutine) by wgengine when its link monitor
// detects a network change.
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -161,7 +150,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
networkUp := ifst.AnyInterfaceUp()
if b.c != nil {
go b.c.SetPaused(b.state == ipn.Stopped || !networkUp)
go b.c.SetPaused(b.state == Stopped || !networkUp)
}
// If the PAC-ness of the network changed, reconfig wireguard+route to
@@ -169,16 +158,12 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
if hadPAC != ifst.HasPAC() {
b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC())
switch b.state {
case ipn.NoState, ipn.Stopped:
case NoState, Stopped:
// Do nothing.
default:
go b.authReconfig()
}
}
// If the local network configuration has changed, our filter may
// need updating to tweak default routes.
b.updateFilter(b.netMap, b.prefs)
}
// Shutdown halts the backend and all its sub-components. The backend
@@ -188,7 +173,6 @@ func (b *LocalBackend) Shutdown() {
cli := b.c
b.mu.Unlock()
b.unregisterLinkMon()
if cli != nil {
cli.Shutdown()
}
@@ -213,7 +197,6 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
defer b.mu.Unlock()
sb.SetBackendState(b.state.String())
sb.SetAuthURL(b.authURL)
// TODO: hostinfo, and its networkinfo
// TODO: EngineStatus copy (and deprecate it?)
@@ -248,26 +231,10 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
Created: p.Created,
LastSeen: lastSeen,
ShareeNode: p.Hostinfo.ShareeNode,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
})
}
}
}
// WhoIs reports the node and user who owns the node with the given IP.
// If ok == true, n and u are valid.
func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ip]
if !ok {
return nil, u, false
}
u, ok = b.netMap.UserProfiles[n.User]
if !ok {
return nil, u, false
}
return n, u, true
}
// SetDecompressor sets a decompression function, which must be a zstd
@@ -297,7 +264,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
b.authReconfig()
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
b.send(Notify{LoginFinished: &empty.Message{}})
}
prefsChanged := false
@@ -322,15 +289,13 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
prefsChanged = true
}
if st.NetMap != nil {
if b.findExitNodeIDLocked(st.NetMap) {
prefsChanged = true
}
b.setNetMapLocked(st.NetMap)
}
if st.URL != "" {
b.authURL = st.URL
}
if b.state == ipn.NeedsLogin {
if b.state == NeedsLogin {
if !b.prefs.WantRunning {
prefsChanged = true
}
@@ -350,7 +315,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.logf("Failed to save new controlclient state: %v", err)
}
}
b.send(ipn.Notify{Prefs: prefs})
b.send(Notify{Prefs: prefs})
}
if st.NetMap != nil {
if netMap != nil {
@@ -369,7 +334,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
}
b.e.SetDERPMap(st.NetMap.DERPMap)
b.send(ipn.Notify{NetMap: st.NetMap})
b.send(Notify{NetMap: st.NetMap})
}
if st.URL != "" {
b.logf("Received auth URL: %.20v...", st.URL)
@@ -383,37 +348,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.authReconfig()
}
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
// rather than by IP. It returns whether prefs was mutated.
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
// If we have a desired IP on file, try to find the corresponding
// node.
if b.prefs.ExitNodeIP.IsZero() {
return false
}
// IP takes precedence over ID, so if both are set, clear ID.
if b.prefs.ExitNodeID != "" {
b.prefs.ExitNodeID = ""
prefsChanged = true
}
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses {
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
continue
}
// Found the node being referenced, upgrade prefs to
// reference it directly for next time.
b.prefs.ExitNodeID = peer.StableID
b.prefs.ExitNodeIP = netaddr.IP{}
return true
}
}
return false
}
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
// This updates the endpoints both in the backend and in the control client.
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
@@ -442,7 +376,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
b.statusChanged.Broadcast()
b.statusLock.Unlock()
b.send(ipn.Notify{Engine: &es})
b.send(Notify{Engine: &es})
}
// Start applies the configuration specified in opts, and starts the
@@ -455,7 +389,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
// guarantee that switching from one user's state to another is
// actually a supported operation (it should be, but it's very unclear
// from the following whether or not that is a safe transition).
func (b *LocalBackend) Start(opts ipn.Options) error {
func (b *LocalBackend) Start(opts Options) error {
if opts.Prefs == nil && opts.StateKey == "" {
return errors.New("no state key or prefs provided")
}
@@ -488,7 +422,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
hostinfo.NetInfo = b.hostinfo.NetInfo
}
b.hostinfo = hostinfo
b.state = ipn.NoState
b.state = NoState
if err := b.loadStateLocked(opts.StateKey, opts.Prefs, opts.LegacyConfigPath); err != nil {
b.mu.Unlock()
@@ -506,7 +440,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.notify = opts.Notify
b.setNetMapLocked(nil)
persistv := b.prefs.Persist
persist := b.prefs.Persist
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
@@ -539,14 +473,14 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}
var err error
if persistv == nil {
if persist == nil {
// let controlclient initialize it
persistv = &persist.Persist{}
persist = &controlclient.Persist{}
}
cli, err := controlclient.New(controlclient.Options{
MachinePrivateKey: machinePrivKey,
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persistv,
Persist: *persist,
ServerURL: b.serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
@@ -555,7 +489,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
HTTPTestClient: opts.HTTPTestClient,
DiscoPublicKey: discoPublic,
DebugFlags: controlDebugFlags,
LinkMonitor: b.e.GetLinkMonitor(),
})
if err != nil {
return err
@@ -586,8 +519,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
blid := b.backendLogID
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
b.send(ipn.Notify{BackendLogID: &blid})
b.send(ipn.Notify{Prefs: prefs})
b.send(Notify{BackendLogID: &blid})
b.send(Notify{Prefs: prefs})
cli.Login(nil, controlclient.LoginDefault)
return nil
@@ -595,7 +528,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// updateFilter updates the packet filter in wgengine based on the
// given netMap and user preferences.
func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Prefs) {
// NOTE(danderson): keep change detection as the first thing in
// this function. Don't try to optimize by returning early, more
// likely than not you'll just end up breaking the change
@@ -605,105 +538,39 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
haveNetmap = netMap != nil
addrs []netaddr.IPPrefix
packetFilter []filter.Match
localNetsB netaddr.IPSetBuilder
logNetsB netaddr.IPSetBuilder
advRoutes []netaddr.IPPrefix
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
)
// Log traffic for Tailscale IPs.
logNetsB.AddPrefix(tsaddr.CGNATRange())
logNetsB.AddPrefix(tsaddr.TailscaleULARange())
logNetsB.RemovePrefix(tsaddr.ChromeOSVMRange())
if haveNetmap {
addrs = netMap.Addresses
for _, p := range addrs {
localNetsB.AddPrefix(p)
}
packetFilter = netMap.PacketFilter
}
if prefs != nil {
for _, r := range prefs.AdvertiseRoutes {
if r.Bits == 0 {
// When offering a default route to the world, we
// filter out locally reachable LANs, so that the
// default route effectively appears to be a "guest
// wifi": you get internet access, but to additionally
// get LAN access the LAN(s) need to be offered
// explicitly as well.
s, err := shrinkDefaultRoute(r)
if err != nil {
b.logf("computing default route filter: %v", err)
continue
}
localNetsB.AddSet(s)
} else {
localNetsB.AddPrefix(r)
// When advertising a non-default route, we assume
// this is a corporate subnet that should be present
// in the audit logs.
logNetsB.AddPrefix(r)
}
}
advRoutes = prefs.AdvertiseRoutes
}
localNets := localNetsB.IPSet()
logNets := logNetsB.IPSet()
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, advRoutes, shieldsUp)
if !changed {
return
}
if !haveNetmap {
b.logf("netmap packet filter: (not ready yet)")
b.e.SetFilter(filter.NewAllowNone(b.logf, logNets))
b.e.SetFilter(filter.NewAllowNone(b.logf))
return
}
oldFilter := b.e.GetFilter()
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
if shieldsUp {
b.logf("netmap packet filter: (shields up)")
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
b.e.SetFilter(filter.NewShieldsUpFilter(b.logf))
} else {
b.logf("netmap packet filter: %v", packetFilter)
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
}
}
var removeFromDefaultRoute = []netaddr.IPPrefix{
// RFC1918 LAN ranges
netaddr.MustParseIPPrefix("192.168.0.0/16"),
netaddr.MustParseIPPrefix("172.16.0.0/12"),
netaddr.MustParseIPPrefix("10.0.0.0/8"),
// Tailscale IPv4 range
tsaddr.CGNATRange(),
// IPv6 Link-local addresses
netaddr.MustParseIPPrefix("fe80::/10"),
// Tailscale IPv6 range
tsaddr.TailscaleULARange(),
}
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
// minus those in removeFromDefaultRoute and local interface subnets.
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
var b netaddr.IPSetBuilder
b.AddPrefix(route)
err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
if tsaddr.IsTailscaleIP(pfx.IP) {
return
}
if pfx.IsSingleIP() {
return
}
b.RemovePrefix(pfx)
})
if err != nil {
return nil, err
}
for _, pfx := range removeFromDefaultRoute {
b.RemovePrefix(pfx)
}
return b.IPSet(), nil
}
// dnsCIDRsEqual determines whether two CIDR lists are equal
// for DNS map construction purposes (that is, only the first entry counts).
func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
@@ -719,7 +586,7 @@ func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
// dnsMapsEqual determines whether the new and the old network map
// induce the same DNS map. It does so without allocating memory,
// at the expense of giving false negatives if peers are reordered.
func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
func dnsMapsEqual(new, old *controlclient.NetworkMap) bool {
if (old == nil) != (new == nil) {
return false
}
@@ -753,7 +620,7 @@ func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
// updateDNSMap updates the domain map in the DNS resolver in wgengine
// based on the given netMap and user preferences.
func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) {
func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
if netMap == nil {
b.logf("dns map: (not ready)")
return
@@ -817,7 +684,7 @@ func (b *LocalBackend) readPoller() {
// send delivers n to the connected frontend. If no frontend is
// connected, the notification is dropped without being delivered.
func (b *LocalBackend) send(n ipn.Notify) {
func (b *LocalBackend) send(n Notify) {
b.mu.Lock()
notify := b.notify
b.mu.Unlock()
@@ -843,9 +710,9 @@ func (b *LocalBackend) popBrowserAuthNow() {
b.blockEngineUpdates(true)
b.stopEngineAndWait()
b.send(ipn.Notify{BrowseToURL: &url})
if b.State() == ipn.Running {
b.enterState(ipn.Starting)
b.send(Notify{BrowseToURL: &url})
if b.State() == Running {
b.enterState(Starting)
}
}
@@ -876,21 +743,21 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
}
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
keyText, err := b.store.ReadState(MachineKeyStateKey)
if err == nil {
if err := b.machinePrivKey.UnmarshalText(keyText); err != nil {
return fmt.Errorf("invalid key in %s key of %v: %w", ipn.MachineKeyStateKey, b.store, err)
return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err)
}
if b.machinePrivKey.IsZero() {
return fmt.Errorf("invalid zero key stored in %v key of %v", ipn.MachineKeyStateKey, b.store)
return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store)
}
if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) {
b.logf("frontend-provided legacy machine key ignored; used value from server state")
}
return nil
}
if err != ipn.ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", ipn.MachineKeyStateKey, b.store, err)
if err != ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err)
}
// If we didn't find one already on disk and the prefs already
@@ -913,7 +780,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
}
keyText, _ = b.machinePrivKey.MarshalText()
if err := b.store.WriteState(ipn.MachineKeyStateKey, keyText); err != nil {
if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil {
b.logf("error writing machine key to store: %v", err)
return err
}
@@ -926,14 +793,14 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
// user and prefs. If userID is blank or prefs is blank, no work is done.
//
// b.mu may either be held or not.
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs) {
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
if userID == "" || prefs == nil {
return
}
if prefs.ForceDaemon {
stateKey := ipn.StateKey("user-" + userID)
if err := b.store.WriteState(ipn.ServerModeStartKey, []byte(stateKey)); err != nil {
stateKey := StateKey("user-" + userID)
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
b.logf("WriteState error: %v", err)
}
// It's important we do this here too, even if it looks
@@ -945,7 +812,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs
b.logf("WriteState error: %v", err)
}
} else {
if err := b.store.WriteState(ipn.ServerModeStartKey, nil); err != nil {
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
b.logf("WriteState error: %v", err)
}
}
@@ -954,7 +821,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs
// loadStateLocked sets b.prefs and b.stateKey based on a complex
// combination of key, prefs, and legacyPath. b.mu must be held when
// calling.
func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legacyPath string) (err error) {
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) {
if prefs == nil && key == "" {
panic("state key and prefs are both unset")
}
@@ -996,19 +863,19 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legac
b.logf("using backend prefs")
bs, err := b.store.ReadState(key)
if err != nil {
if errors.Is(err, ipn.ErrStateNotExist) {
if errors.Is(err, ErrStateNotExist) {
if legacyPath != "" {
b.prefs, err = ipn.LoadPrefs(legacyPath)
b.prefs, err = LoadPrefs(legacyPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
b.logf("failed to load legacy prefs: %v", err)
}
b.prefs = ipn.NewPrefs()
b.prefs = NewPrefs()
} else {
b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty())
}
} else {
b.prefs = ipn.NewPrefs()
b.prefs = NewPrefs()
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
}
if err := b.initMachineKeyLocked(); err != nil {
@@ -1018,7 +885,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legac
}
return fmt.Errorf("store.ReadState(%q): %v", key, err)
}
b.prefs, err = ipn.PrefsFromBytes(bs, false)
b.prefs, err = PrefsFromBytes(bs, false)
if err != nil {
return fmt.Errorf("PrefsFromBytes: %v", err)
}
@@ -1030,7 +897,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legac
}
// State returns the backend state machine's current state.
func (b *LocalBackend) State() ipn.State {
func (b *LocalBackend) State() State {
b.mu.Lock()
defer b.mu.Unlock()
@@ -1046,7 +913,7 @@ func (b *LocalBackend) InServerMode() bool {
// getEngineStatus returns a copy of b.engineStatus.
//
// TODO(bradfitz): remove this and use Status() throughout.
func (b *LocalBackend) getEngineStatus() ipn.EngineStatus {
func (b *LocalBackend) getEngineStatus() EngineStatus {
b.mu.Lock()
defer b.mu.Unlock()
@@ -1102,7 +969,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
mapCopy.Expiry = time.Now().Add(x)
}
b.setNetMapLocked(&mapCopy)
b.send(ipn.Notify{NetMap: b.netMap})
b.send(Notify{NetMap: b.netMap})
}
func (b *LocalBackend) Ping(ipStr string) {
@@ -1112,7 +979,7 @@ func (b *LocalBackend) Ping(ipStr string) {
return
}
b.e.Ping(ip, func(pr *ipnstate.PingResult) {
b.send(ipn.Notify{PingResult: pr})
b.send(Notify{PingResult: pr})
})
}
@@ -1121,11 +988,11 @@ func (b *LocalBackend) Ping(ipStr string) {
// b.mu must be held; mostly because the caller is about to anyway, and doing so
// gives us slightly better guarantees about the two peers stats lines not
// being intermixed if there are concurrent calls to our caller.
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineStatus) {
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) {
var peerStats, peerKeys strings.Builder
ret.LiveDERPs = s.DERPs
ret.LivePeers = map[tailcfg.NodeKey]ipnstate.PeerStatusLite{}
ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{}
for _, p := range s.Peers {
if !p.LastHandshake.IsZero() {
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
@@ -1181,7 +1048,7 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) {
// SetPrefs saves new user preferences and propagates them throughout
// the system. Implements Backend.
func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
func (b *LocalBackend) SetPrefs(newp *Prefs) {
if newp == nil {
panic("SetPrefs got nil prefs")
}
@@ -1248,7 +1115,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
b.authReconfig()
}
b.send(ipn.Notify{Prefs: newp})
b.send(Notify{Prefs: newp})
}
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
@@ -1274,7 +1141,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
// NetMap returns the latest cached network map received from
// controlclient, or nil if no network map was received yet.
func (b *LocalBackend) NetMap() *netmap.NetworkMap {
func (b *LocalBackend) NetMap() *controlclient.NetworkMap {
b.mu.Lock()
defer b.mu.Unlock()
return b.netMap
@@ -1316,21 +1183,23 @@ func (b *LocalBackend) authReconfig() {
return
}
var flags netmap.WGConfigFlags
var flags controlclient.WGConfigFlags
if uc.RouteAll {
flags |= netmap.AllowSubnetRoutes
flags |= controlclient.AllowDefaultRoute
// TODO(apenwarr): Make subnet routes a different pref?
flags |= controlclient.AllowSubnetRoutes
}
if uc.AllowSingleHosts {
flags |= netmap.AllowSingleHosts
flags |= controlclient.AllowSingleHosts
}
if hasPAC && disableSubnetsIfPAC {
if flags&netmap.AllowSubnetRoutes != 0 {
if flags&controlclient.AllowSubnetRoutes != 0 {
b.logf("authReconfig: have PAC; disabling subnet routes")
flags &^= netmap.AllowSubnetRoutes
flags &^= controlclient.AllowSubnetRoutes
}
}
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, uc.ExitNodeID)
cfg, err := nm.WGCfg(b.logf, flags)
if err != nil {
b.logf("wgcfg: %v", err)
return
@@ -1362,87 +1231,24 @@ func (b *LocalBackend) authReconfig() {
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
// Each entry has a trailing period.
func magicDNSRootDomains(nm *netmap.NetworkMap) []string {
func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
if v := nm.MagicDNSSuffix(); v != "" {
return []string{strings.Trim(v, ".") + "."}
}
return nil
}
var (
ipv4Default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6Default = netaddr.MustParseIPPrefix("::/0")
)
// peerRoutes returns the routerConfig.Routes to access peers.
// If there are over cgnatThreshold CGNAT routes, one big CGNAT route
// is used instead.
func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netaddr.IPPrefix) {
tsULA := tsaddr.TailscaleULARange()
cgNAT := tsaddr.CGNATRange()
var didULA bool
var cgNATIPs []netaddr.IPPrefix
for _, peer := range peers {
for _, aip := range peer.AllowedIPs {
aip = unmapIPPrefix(aip)
// Only add the Tailscale IPv6 ULA once, if we see anybody using part of it.
if aip.IP.Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP) {
if !didULA {
didULA = true
routes = append(routes, tsULA)
}
continue
}
if aip.IsSingleIP() && cgNAT.Contains(aip.IP) {
cgNATIPs = append(cgNATIPs, aip)
} else {
routes = append(routes, aip)
}
}
}
if len(cgNATIPs) > cgnatThreshold {
// Probably the hello server. Just append one big route.
routes = append(routes, cgNAT)
} else {
routes = append(routes, cgNATIPs...)
}
return routes
}
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
rs := &router.Config{
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
SNATSubnetRoutes: !prefs.NoSNAT,
NetfilterMode: prefs.NetfilterMode,
Routes: peerRoutes(cfg.Peers, 10_000),
}
// Sanity check: we expect the control server to program both a v4
// and a v6 default route, if default routing is on. Fill in
// blackhole routes appropriately if we're missing some. This is
// likely to break some functionality, but if the user expressed a
// preference for routing remotely, we want to avoid leaking
// traffic at the expense of functionality.
if prefs.ExitNodeID != "" || !prefs.ExitNodeIP.IsZero() {
var default4, default6 bool
for _, route := range rs.Routes {
if route == ipv4Default {
default4 = true
} else if route == ipv6Default {
default6 = true
}
if default4 && default6 {
break
}
}
if !default4 {
rs.Routes = append(rs.Routes, ipv4Default)
}
if !default6 {
rs.Routes = append(rs.Routes, ipv6Default)
}
for _, peer := range cfg.Peers {
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
}
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
@@ -1453,20 +1259,16 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
return rs
}
func unmapIPPrefix(ipp netaddr.IPPrefix) netaddr.IPPrefix {
return netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits}
}
func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
for _, ipps := range ippsList {
for _, ipp := range ipps {
ret = append(ret, unmapIPPrefix(ipp))
ret = append(ret, netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits})
}
}
return ret
}
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
if h := prefs.Hostname; h != "" {
hi.Hostname = h
}
@@ -1486,7 +1288,7 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
// places twiddle IPN internal state without going through here, so
// really this is more "one of several places in which random things
// happen".
func (b *LocalBackend) enterState(newState ipn.State) {
func (b *LocalBackend) enterState(newState State) {
b.mu.Lock()
state := b.state
b.state = newState
@@ -1504,19 +1306,19 @@ func (b *LocalBackend) enterState(newState ipn.State) {
b.logf("Switching ipn state %v -> %v (WantRunning=%v)",
state, newState, prefs.WantRunning)
if notify != nil {
b.send(ipn.Notify{State: &newState})
b.send(Notify{State: &newState})
}
if bc != nil {
bc.SetPaused(newState == ipn.Stopped || !networkUp)
bc.SetPaused(newState == Stopped || !networkUp)
}
switch newState {
case ipn.NeedsLogin:
case NeedsLogin:
systemd.Status("Needs login: %s", authURL)
b.blockEngineUpdates(true)
fallthrough
case ipn.Stopped:
case Stopped:
err := b.e.Reconfig(&wgcfg.Config{}, &router.Config{})
if err != nil {
b.logf("Reconfig(down): %v", err)
@@ -1525,11 +1327,11 @@ func (b *LocalBackend) enterState(newState ipn.State) {
if authURL == "" {
systemd.Status("Stopped; run 'tailscale up' to log in")
}
case ipn.Starting, ipn.NeedsMachineAuth:
case Starting, NeedsMachineAuth:
b.authReconfig()
// Needed so that UpdateEndpoints can run
b.e.RequestStatus()
case ipn.Running:
case Running:
var addrs []string
for _, addr := range b.netMap.Addresses {
addrs = append(addrs, addr.IP.String())
@@ -1543,7 +1345,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
// nextState returns the state the backend seems to be in, based on
// its internal state.
func (b *LocalBackend) nextState() ipn.State {
func (b *LocalBackend) nextState() State {
b.mu.Lock()
b.assertClientLocked()
var (
@@ -1559,31 +1361,31 @@ func (b *LocalBackend) nextState() ipn.State {
if c.AuthCantContinue() {
// Auth was interrupted or waiting for URL visit,
// so it won't proceed without human help.
return ipn.NeedsLogin
return NeedsLogin
} else {
// Auth or map request needs to finish
return state
}
case !wantRunning:
return ipn.Stopped
return Stopped
case !netMap.Expiry.IsZero() && time.Until(netMap.Expiry) <= 0:
return ipn.NeedsLogin
return NeedsLogin
case netMap.MachineStatus != tailcfg.MachineAuthorized:
// TODO(crawshaw): handle tailcfg.MachineInvalid
return ipn.NeedsMachineAuth
case state == ipn.NeedsMachineAuth:
return NeedsMachineAuth
case state == NeedsMachineAuth:
// (if we get here, we know MachineAuthorized == true)
return ipn.Starting
case state == ipn.Starting:
return Starting
case state == Starting:
if st := b.getEngineStatus(); st.NumLive > 0 || st.LiveDERPs > 0 {
return ipn.Running
return Running
} else {
return state
}
case state == ipn.Running:
return ipn.Running
case state == Running:
return Running
default:
return ipn.Starting
return Starting
}
}
@@ -1595,7 +1397,7 @@ func (b *LocalBackend) RequestEngineStatus() {
// RequestStatus implements Backend.
func (b *LocalBackend) RequestStatus() {
st := b.Status()
b.send(ipn.Notify{Status: st})
b.send(Notify{Status: st})
}
// stateMachine updates the state machine state based on other things
@@ -1691,7 +1493,7 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
c.SetNetInfo(ni)
}
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
var login string
if nm != nil {
login = nm.UserProfiles[nm.User].LoginName
@@ -1704,39 +1506,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.logf("active login: %v", login)
b.activeLogin = login
}
if nm == nil {
b.nodeByAddr = nil
return
}
// Update the nodeByAddr index.
if b.nodeByAddr == nil {
b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{}
}
// First pass, mark everything unwanted.
for k := range b.nodeByAddr {
b.nodeByAddr[k] = nil
}
addNode := func(n *tailcfg.Node) {
for _, ipp := range n.Addresses {
if ipp.IsSingleIP() {
b.nodeByAddr[ipp.IP] = n
}
}
}
if nm.SelfNode != nil {
addNode(nm.SelfNode)
}
for _, p := range nm.Peers {
addNode(p)
}
// Third pass, actually delete the unwanted items.
for k, v := range b.nodeByAddr {
if v == nil {
delete(b.nodeByAddr, k)
}
}
}
// TestOnlyPublicKeys returns the current machine and node public

119
ipn/local_test.go Normal file
View File

@@ -0,0 +1,119 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
import (
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/tailcfg"
"testing"
)
func TestNetworkMapCompare(t *testing.T) {
prefix1, err := netaddr.ParseIPPrefix("192.168.0.0/24")
if err != nil {
t.Fatal(err)
}
node1 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix1}}
prefix2, err := netaddr.ParseIPPrefix("10.0.0.0/8")
if err != nil {
t.Fatal(err)
}
node2 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix2}}
tests := []struct {
name string
a, b *controlclient.NetworkMap
want bool
}{
{
"both nil",
nil,
nil,
true,
},
{
"b nil",
&controlclient.NetworkMap{},
nil,
false,
},
{
"a nil",
nil,
&controlclient.NetworkMap{},
false,
},
{
"both default",
&controlclient.NetworkMap{},
&controlclient.NetworkMap{},
true,
},
{
"names identical",
&controlclient.NetworkMap{Name: "map1"},
&controlclient.NetworkMap{Name: "map1"},
true,
},
{
"names differ",
&controlclient.NetworkMap{Name: "map1"},
&controlclient.NetworkMap{Name: "map2"},
false,
},
{
"Peers identical",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
true,
},
{
"Peer list length",
// length of Peers list differs
&controlclient.NetworkMap{Peers: []*tailcfg.Node{{}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
false,
},
{
"Node names identical",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
true,
},
{
"Node names differ",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
false,
},
{
"Node lists identical",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
true,
},
{
"Node lists differ",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
false,
},
{
"Node Users differ",
// User field is not checked.
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
true,
},
}
for _, tt := range tests {
got := dnsMapsEqual(tt.a, tt.b)
if got != tt.want {
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
}
}
}

View File

@@ -1,111 +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.
// Package localapi contains the HTTP server handlers for tailscaled's API server.
package localapi
import (
"encoding/json"
"io"
"net/http"
"runtime"
"inet.af/netaddr"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
)
func NewHandler(b *ipnlocal.LocalBackend) *Handler {
return &Handler{b: b}
}
type Handler struct {
// RequiredPassword, if non-empty, forces all HTTP
// requests to have HTTP basic auth with this password.
// It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
RequiredPassword string
// PermitRead is whether read-only HTTP handlers are allowed.
PermitRead bool
// PermitWrite is whether mutating HTTP handlers are allowed.
PermitWrite bool
b *ipnlocal.LocalBackend
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.b == nil {
http.Error(w, "server has no local backend", http.StatusInternalServerError)
return
}
if h.RequiredPassword != "" {
_, pass, ok := r.BasicAuth()
if !ok {
http.Error(w, "auth required", http.StatusUnauthorized)
return
}
if pass != h.RequiredPassword {
http.Error(w, "bad password", http.StatusForbidden)
return
}
}
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)
case "/localapi/v0/goroutines":
h.serveGoroutines(w, r)
default:
io.WriteString(w, "tailscaled\n")
}
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden)
return
}
b := h.b
var ip netaddr.IP
if v := r.FormValue("ip"); v != "" {
var err error
ip, err = netaddr.ParseIP(r.FormValue("ip"))
if err != nil {
http.Error(w, "invalid 'ip' parameter", 400)
return
}
} else {
http.Error(w, "missing 'ip' parameter", 400)
return
}
n, u, ok := b.WhoIs(ip)
if !ok {
http.Error(w, "no match for IP", 404)
return
}
res := &tailcfg.WhoIsResponse{
Node: n,
UserProfile: &u,
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}
func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
// Require write access out of paranoia that the goroutine dump
// (at least its arguments) might contain something sensitive.
if !h.PermitWrite {
http.Error(w, "goroutine dump access denied", http.StatusForbidden)
return
}
buf := make([]byte, 2<<20)
buf = buf[:runtime.Stack(buf, true)]
w.Header().Set("Content-Type", "text/plain")
w.Write(buf)
}

View File

@@ -2,20 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnlocal
package ipn
import (
"reflect"
"testing"
"time"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/control/controlclient"
"tailscale.com/logtail"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/persist"
"tailscale.com/wgengine"
)
@@ -40,8 +38,10 @@ func TestLocalLogLines(t *testing.T) {
idA := logid(0xaa)
// set up a LocalBackend, super bare bones. No functional data.
store := &ipn.MemoryStore{}
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
store := &MemoryStore{
cache: make(map[StateKey][]byte),
}
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil)
if err != nil {
t.Fatal(err)
}
@@ -53,7 +53,7 @@ func TestLocalLogLines(t *testing.T) {
defer lb.Shutdown()
// custom adjustments for required non-nil fields
lb.prefs = ipn.NewPrefs()
lb.prefs = NewPrefs()
lb.hostinfo = &tailcfg.Hostinfo{}
// hacky manual override of the usual log-on-change behaviour of keylogf
lb.keyLogf = logListen.Logf
@@ -67,8 +67,8 @@ func TestLocalLogLines(t *testing.T) {
}
// log prefs line
persist := &persist.Persist{}
prefs := ipn.NewPrefs()
persist := &controlclient.Persist{}
prefs := NewPrefs()
prefs.Persist = persist
lb.SetPrefs(prefs)
@@ -76,7 +76,7 @@ func TestLocalLogLines(t *testing.T) {
// log peers, peer keys
status := &wgengine.Status{
Peers: []ipnstate.PeerStatusLite{{
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
TxBytes: 10,
RxBytes: 10,
LastHandshake: time.Now(),

View File

@@ -146,10 +146,6 @@ func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error
return bs.GotCommand(ctx, cmd)
}
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
// operation was done from a user/context that didn't have permission.
const ErrMsgPermissionDenied = "permission denied"
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
@@ -182,7 +178,7 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
}
if IsReadonlyContext(ctx) {
msg := ErrMsgPermissionDenied
msg := "permission denied"
bs.send(Notify{ErrMessage: &msg})
return nil
}

View File

@@ -16,7 +16,9 @@ import (
func TestReadWrite(t *testing.T) {
tstest.PanicOnLog()
tstest.ResourceCheck(t)
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
buf := bytes.Buffer{}
err := WriteMsg(&buf, []byte("Test string1"))
@@ -62,7 +64,9 @@ func TestReadWrite(t *testing.T) {
func TestClientServer(t *testing.T) {
tstest.PanicOnLog()
tstest.ResourceCheck(t)
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
b := &FakeBackend{}
var bs *BackendServer

View File

@@ -17,9 +17,8 @@ import (
"inet.af/netaddr"
"tailscale.com/atomicfile"
"tailscale.com/tailcfg"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/control/controlclient"
"tailscale.com/wgengine/router"
)
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
@@ -29,10 +28,8 @@ type Prefs struct {
// ControlURL is the URL of the control server to use.
ControlURL string
// RouteAll specifies whether to accept subnets advertised by
// other nodes on the Tailscale network. Note that this does not
// include default routes (0.0.0.0/0 and ::/0), those are
// controlled by ExitNodeID/IP below.
// RouteAll specifies whether to accept subnet and default routes
// advertised by other nodes on the Tailscale network.
RouteAll bool
// AllowSingleHosts specifies whether to install routes for each
@@ -47,24 +44,6 @@ type Prefs struct {
// packets stop flowing. What's up with that?
AllowSingleHosts bool
// ExitNodeID and ExitNodeIP specify the node that should be used
// as an exit node for internet traffic. At most one of these
// should be non-zero.
//
// The preferred way to express the chosen node is ExitNodeID, but
// in some cases it's not possible to use that ID (e.g. in the
// linux CLI, before tailscaled has a netmap). For those
// situations, we allow specifying the exit node by IP, and
// ipnlocal.LocalBackend will translate the IP into an ID when the
// node is found in the netmap.
//
// If the selected exit node doesn't exist (e.g. it's not part of
// the current tailnet), or it doesn't offer exit node services, a
// blackhole route will be installed on the local system to
// prevent any traffic escaping to the local network.
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists.
CorpDNS bool
@@ -137,14 +116,14 @@ type Prefs struct {
// NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all.
NetfilterMode preftype.NetfilterMode
NetfilterMode router.NetfilterMode
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
// We can maybe do that once we're sure which module should persist
// it (backend or frontend?)
Persist *persist.Persist `json:"Config"`
Persist *controlclient.Persist `json:"Config"`
}
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
@@ -168,11 +147,6 @@ func (p *Prefs) pretty(goos string) string {
if p.ShieldsUp {
sb.WriteString("shields=true ")
}
if !p.ExitNodeIP.IsZero() {
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeIP)
} else if !p.ExitNodeID.IsZero() {
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeID)
}
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
}
@@ -217,8 +191,6 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.ControlURL == p2.ControlURL &&
p.RouteAll == p2.RouteAll &&
p.AllowSingleHosts == p2.AllowSingleHosts &&
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
p.CorpDNS == p2.CorpDNS &&
p.WantRunning == p2.WantRunning &&
p.NotepadURLs == p2.NotepadURLs &&
@@ -268,7 +240,7 @@ func NewPrefs() *Prefs {
AllowSingleHosts: true,
CorpDNS: true,
WantRunning: true,
NetfilterMode: preftype.NetfilterOn,
NetfilterMode: router.NetfilterOn,
}
}
@@ -280,7 +252,7 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
if len(b) == 0 {
return p, nil
}
persist := &persist.Persist{}
persist := &controlclient.Persist{}
err := json.Unmarshal(b, persist)
if err == nil && (persist.Provider != "" || persist.LoginName != "") {
// old-style relaynode config; import it

View File

@@ -8,9 +8,8 @@ package ipn
import (
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/control/controlclient"
"tailscale.com/wgengine/router"
)
// Clone makes a deep copy of Prefs.
@@ -24,7 +23,7 @@ func (src *Prefs) Clone() *Prefs {
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
if dst.Persist != nil {
dst.Persist = new(persist.Persist)
dst.Persist = new(controlclient.Persist)
*dst.Persist = *src.Persist
}
return dst
@@ -36,8 +35,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
CorpDNS bool
WantRunning bool
ShieldsUp bool
@@ -49,6 +46,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
ForceDaemon bool
AdvertiseRoutes []netaddr.IPPrefix
NoSNAT bool
NetfilterMode preftype.NetfilterMode
Persist *persist.Persist
NetfilterMode router.NetfilterMode
Persist *controlclient.Persist
}{})

View File

@@ -14,11 +14,10 @@ import (
"time"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/control/controlclient"
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/router"
)
func fieldsOf(t reflect.Type) (fields []string) {
@@ -31,7 +30,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles)
@@ -100,28 +99,6 @@ func TestPrefsEqual(t *testing.T) {
true,
},
{
&Prefs{ExitNodeID: "n1234"},
&Prefs{},
false,
},
{
&Prefs{ExitNodeID: "n1234"},
&Prefs{ExitNodeID: "n1234"},
true,
},
{
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
&Prefs{},
false,
},
{
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
true,
},
{
&Prefs{CorpDNS: true},
&Prefs{CorpDNS: false},
@@ -215,24 +192,24 @@ func TestPrefsEqual(t *testing.T) {
},
{
&Prefs{NetfilterMode: preftype.NetfilterOff},
&Prefs{NetfilterMode: preftype.NetfilterOn},
&Prefs{NetfilterMode: router.NetfilterOff},
&Prefs{NetfilterMode: router.NetfilterOn},
false,
},
{
&Prefs{NetfilterMode: preftype.NetfilterOn},
&Prefs{NetfilterMode: preftype.NetfilterOn},
&Prefs{NetfilterMode: router.NetfilterOn},
&Prefs{NetfilterMode: router.NetfilterOn},
true,
},
{
&Prefs{Persist: &persist.Persist{}},
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
&Prefs{Persist: &controlclient.Persist{}},
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
false,
},
{
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
true,
},
}
@@ -297,7 +274,7 @@ func TestBasicPrefs(t *testing.T) {
func TestPrefsPersist(t *testing.T) {
tstest.PanicOnLog()
c := persist.Persist{
c := controlclient.Persist{
LoginName: "test@example.com",
}
p := Prefs{
@@ -363,34 +340,20 @@ func TestPrefsPretty(t *testing.T) {
},
{
Prefs{
Persist: &persist.Persist{},
Persist: &controlclient.Persist{},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
},
{
Prefs{
Persist: &persist.Persist{
Persist: &controlclient.Persist{
PrivateNodeKey: wgkey.Private{1: 1},
},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
},
{
Prefs{
ExitNodeIP: netaddr.MustParseIP("1.2.3.4"),
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 routes=[] nf=off Persist=nil}`,
},
{
Prefs{
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC routes=[] nf=off Persist=nil}`,
},
}
for i, tt := range tests {
got := tt.p.pretty(tt.os)

View File

@@ -17,7 +17,6 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@@ -339,18 +338,6 @@ func New(collection string) *Policy {
tryFixLogStateLocation(dir, cmdName)
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
// The Windows service previously ran as tailscale-ipn.exe, so
// let's keep using that log base name if it exists.
if runtime.GOOS == "windows" && cmdName == "tailscaled" {
const oldCmdName = "tailscale-ipn"
oldPath := filepath.Join(dir, oldCmdName+".log.conf")
if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() {
cfgPath = oldPath
cmdName = oldCmdName
}
}
var oldc *Config
data, err := ioutil.ReadFile(cfgPath)
if err != nil {
@@ -400,13 +387,6 @@ func New(collection string) *Policy {
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
}
if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok {
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
c.BaseURL = val
u, _ := url.Parse(val)
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
}
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
if filchBuf != nil {
c.Buffer = filchBuf

View File

@@ -18,9 +18,7 @@ import (
"time"
"tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces"
tslogger "tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
// DefaultHost is the default host name to upload logs to when
@@ -108,7 +106,6 @@ type Logger struct {
url string
lowMem bool
skipClientTime bool
linkMonitor *monitor.Mon
buffer Buffer
sent chan struct{} // signal to speed up drain
drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain
@@ -131,14 +128,6 @@ func (l *Logger) SetVerbosityLevel(level int) {
l.stderrLevel = level
}
// SetLinkMonitor sets the optional the link monitor.
//
// It should not be changed concurrently with log writes and should
// only be set once.
func (l *Logger) SetLinkMonitor(lm *monitor.Mon) {
l.linkMonitor = lm
}
// Shutdown gracefully shuts down the logger while completing any
// remaining uploads.
//
@@ -288,11 +277,6 @@ func (l *Logger) uploading(ctx context.Context) {
}
uploaded, err := l.upload(ctx, body, origlen)
if err != nil {
if !l.internetUp() {
fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n")
l.awaitInternetUp(ctx)
continue
}
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
}
l.bo.BackOff(ctx, err)
@@ -309,34 +293,6 @@ func (l *Logger) uploading(ctx context.Context) {
}
}
func (l *Logger) internetUp() bool {
if l.linkMonitor == nil {
// No way to tell, so assume it is.
return true
}
return l.linkMonitor.InterfaceState().AnyInterfaceUp()
}
func (l *Logger) awaitInternetUp(ctx context.Context) {
upc := make(chan bool, 1)
defer l.linkMonitor.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if st.AnyInterfaceUp() {
select {
case upc <- true:
default:
}
}
})()
if l.internetUp() {
return
}
select {
case <-upc:
fmt.Fprintf(l.stderr, "logtail: internet back up\n")
case <-ctx.Done():
}
}
// upload uploads body to the log server.
// origlen indicates the pre-compression body length.
// origlen of -1 indicates that the body is not compressed.

View File

@@ -8,8 +8,6 @@ package dnscache
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
@@ -20,7 +18,6 @@ import (
"time"
"golang.org/x/sync/singleflight"
"inet.af/netaddr"
)
var single = &Resolver{
@@ -58,10 +55,6 @@ type Resolver struct {
// If nil, net.DefaultResolver is used.
Forward *net.Resolver
// LookupIPFallback optionally provides a backup DNS mechanism
// to use if Forward returns an error or no results.
LookupIPFallback func(ctx context.Context, host string) ([]netaddr.IP, error)
// TTL is how long to keep entries cached
//
// If zero, a default (currently 10 minutes) is used.
@@ -205,18 +198,6 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
defer cancel()
ips, err := r.fwd().LookupIPAddr(ctx, host)
if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var fips []netaddr.IP
fips, err = r.LookupIPFallback(ctx, host)
if err == nil {
ips = nil
for _, fip := range fips {
ips = append(ips, *fip.IPAddr())
}
}
}
if err != nil {
return nil, nil, err
}
@@ -288,33 +269,13 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
return func(ctx context.Context, network, address string) (retConn net.Conn, ret error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
// Bogus. But just let the real dialer return an error rather than
// inventing a similar one.
return fwd(ctx, network, address)
}
defer func() {
// On any failure, assume our DNS is wrong and try our fallback, if any.
if ret == nil || dnsCache.LookupIPFallback == nil {
return
}
ips, err := dnsCache.LookupIPFallback(ctx, host)
if err != nil {
// Return with original error
return
}
for _, ip := range ips {
dst := net.JoinHostPort(ip.String(), port)
if c, err := fwd(ctx, network, dst); err == nil {
retConn = c
ret = nil
return
}
}
}()
ip, ip6, err := dnsCache.LookupIP(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
@@ -339,62 +300,3 @@ func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
return fwd(ctx, network, dst)
}
}
var errTLSHandshakeTimeout = errors.New("timeout doing TLS handshake")
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
// It returns a *tls.Conn type on success.
// On TLS cert validation failure, it can invoke a backup DNS resolution strategy.
func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Config) DialContextFunc {
tcpDialer := Dialer(fwd, dnsCache)
return func(ctx context.Context, network, address string) (net.Conn, error) {
host, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
tcpConn, err := tcpDialer(ctx, network, address)
if err != nil {
return nil, err
}
cfg := cloneTLSConfig(tlsConfigBase)
if cfg.ServerName == "" {
cfg.ServerName = host
}
tlsConn := tls.Client(tcpConn, cfg)
errc := make(chan error, 2)
handshakeCtx, handshakeTimeoutCancel := context.WithTimeout(ctx, 5*time.Second)
defer handshakeTimeoutCancel()
done := make(chan bool)
defer close(done)
go func() {
select {
case <-done:
case <-handshakeCtx.Done():
errc <- errTLSHandshakeTimeout
}
}()
go func() {
err := tlsConn.Handshake()
handshakeTimeoutCancel()
errc <- err
}()
if err := <-errc; err != nil {
tcpConn.Close()
// TODO: if err != errTLSHandshakeTimeout,
// assume it might be some captive portal or
// otherwise incorrect DNS and try the backup
// DNS mechanism.
return nil, err
}
return tlsConn, nil
}
}
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}

View File

@@ -1,117 +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.
// Package dnsfallback contains a DNS fallback mechanism
// for starting up Tailscale when the system DNS is broken or otherwise unavailable.
package dnsfallback
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"time"
"inet.af/netaddr"
"tailscale.com/derp/derpmap"
"tailscale.com/net/netns"
"tailscale.com/net/tshttpproxy"
)
func Lookup(ctx context.Context, host string) ([]netaddr.IP, error) {
type nameIP struct {
dnsName string
ip netaddr.IP
}
dm := derpmap.Prod()
var cands4, cands6 []nameIP
for _, dr := range dm.Regions {
for _, n := range dr.Nodes {
if ip, err := netaddr.ParseIP(n.IPv4); err == nil {
cands4 = append(cands4, nameIP{n.HostName, ip})
}
if ip, err := netaddr.ParseIP(n.IPv6); err == nil {
cands6 = append(cands6, nameIP{n.HostName, ip})
}
}
}
rand.Shuffle(len(cands4), func(i, j int) { cands4[i], cands4[j] = cands4[j], cands4[i] })
rand.Shuffle(len(cands6), func(i, j int) { cands6[i], cands6[j] = cands6[j], cands6[i] })
const maxCands = 6
var cands []nameIP // up to maxCands alternating v4/v6 as long as we have both
for (len(cands4) > 0 || len(cands6) > 0) && len(cands) < maxCands {
if len(cands4) > 0 {
cands = append(cands, cands4[0])
cands4 = cands4[1:]
}
if len(cands6) > 0 {
cands = append(cands, cands6[0])
cands6 = cands6[1:]
}
}
if len(cands) == 0 {
return nil, fmt.Errorf("no DNS fallback options for %q", host)
}
for _, cand := range cands {
if err := ctx.Err(); err != nil {
return nil, err
}
log.Printf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host)
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host)
if err != nil {
log.Printf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err)
continue
}
if ips := dm[host]; len(ips) > 0 {
log.Printf("bootstrapDNS(%q, %q) for %q = %v", cand.dnsName, cand.ip, host, ips)
return ips, nil
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, fmt.Errorf("no DNS fallback candidates remain for %q", host)
}
// serverName and serverIP of are, say, "derpN.tailscale.com".
// queryName is the name being sought (e.g. "login.tailscale.com"), passed as hint.
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netaddr.IP, queryName string) (dnsMap, error) {
dialer := netns.NewDialer()
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443"))
}
c := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil)
if err != nil {
return nil, err
}
dm := make(dnsMap)
res, err := c.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, errors.New(res.Status)
}
if err := json.NewDecoder(res.Body).Decode(&dm); err != nil {
return nil, err
}
return dm, nil
}
// dnsMap is the JSON type returned by the DERP /bootstrap-dns handler:
// https://derp10.tailscale.com/bootstrap-dns
type dnsMap map[string][]netaddr.IP

View File

@@ -10,7 +10,6 @@ import (
"net"
"net/http"
"reflect"
"runtime"
"sort"
"strings"
@@ -40,11 +39,8 @@ func Tailscale() (net.IP, *net.Interface, error) {
continue
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok {
nip, ok := netaddr.FromStdIP(ipnet.IP)
if ok && tsaddr.IsTailscaleIP(nip) {
return ipnet.IP, &iface, nil
}
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
return ipnet.IP, &iface, nil
}
}
}
@@ -61,24 +57,19 @@ func maybeTailscaleInterfaceName(s string) bool {
strings.HasPrefix(s, "utun")
}
// IsTailscaleIP reports whether ip is an IP in a range used by
// Tailscale virtual network interfaces.
func IsTailscaleIP(ip net.IP) bool {
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
return tsaddr.IsTailscaleIP(nip)
}
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
func isProblematicInterface(nif *net.Interface) bool {
name := nif.Name
// Don't try to send disco/etc packets over zerotier; they effectively
// DoS each other by doing traffic amplification, both of them
// preferring/trying to use each other for transport. See:
// https://github.com/tailscale/tailscale/issues/1208
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
return true
}
return false
}
// LocalAddresses returns the machine's IP addresses, separated by
// whether they're loopback addresses.
func LocalAddresses() (regular, loopback []netaddr.IP, err error) {
func LocalAddresses() (regular, loopback []string, err error) {
// TODO(crawshaw): don't serve interface addresses that we are routing
ifaces, err := net.Interfaces()
if err != nil {
@@ -86,10 +77,8 @@ func LocalAddresses() (regular, loopback []netaddr.IP, err error) {
}
for i := range ifaces {
iface := &ifaces[i]
if !isUp(iface) || isProblematicInterface(iface) {
// Skip down interfaces and ones that are
// problematic that we don't want to try to
// send Tailscale traffic over.
if !isUp(iface) {
// Down interfaces don't count
continue
}
ifcIsLoopback := isLoopback(iface)
@@ -117,22 +106,16 @@ func LocalAddresses() (regular, loopback []netaddr.IP, err error) {
continue
}
if ip.IsLoopback() || ifcIsLoopback {
loopback = append(loopback, ip)
loopback = append(loopback, ip.String())
} else {
regular = append(regular, ip)
regular = append(regular, ip.String())
}
}
}
}
sortIPs(regular)
sortIPs(loopback)
return regular, loopback, nil
}
func sortIPs(s []netaddr.IP) {
sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) })
}
// Interface is a wrapper around Go's net.Interface with some extra methods.
type Interface struct {
*net.Interface
@@ -141,10 +124,8 @@ type Interface struct {
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
func (i Interface) IsUp() bool { return isUp(i.Interface) }
// ForeachInterfaceAddress calls fn for each interface's address on
// the machine. The IPPrefix's IP is the IP address assigned to the
// interface, and Bits are the subnet mask.
func ForeachInterfaceAddress(fn func(Interface, netaddr.IPPrefix)) error {
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
ifaces, err := net.Interfaces()
if err != nil {
return err
@@ -158,8 +139,8 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IPPrefix)) error {
for _, a := range addrs {
switch v := a.(type) {
case *net.IPNet:
if pfx, ok := netaddr.FromStdIPNet(v); ok {
fn(Interface{iface}, pfx)
if ip, ok := netaddr.FromStdIP(v.IP); ok {
fn(Interface{iface}, ip)
}
}
}
@@ -167,43 +148,11 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IPPrefix)) error {
return nil
}
// ForeachInterface calls fn for each interface on the machine, with
// all its addresses. The IPPrefix's IP is the IP address assigned to
// the interface, and Bits are the subnet mask.
func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error {
ifaces, err := net.Interfaces()
if err != nil {
return err
}
for i := range ifaces {
iface := &ifaces[i]
addrs, err := iface.Addrs()
if err != nil {
return err
}
var pfxs []netaddr.IPPrefix
for _, a := range addrs {
switch v := a.(type) {
case *net.IPNet:
if pfx, ok := netaddr.FromStdIPNet(v); ok {
pfxs = append(pfxs, pfx)
}
}
}
fn(Interface{iface}, pfxs)
}
return nil
}
// State is intended to store the state of the machine's network interfaces,
// routing table, and other network configuration.
// For now it's pretty basic.
type State struct {
// InterfaceIPs maps from an interface name to the IP addresses
// configured on that interface. Each address is represented as an
// IPPrefix, where the IP is the interface IP address and Bits is
// the subnet mask.
InterfaceIPs map[string][]netaddr.IPPrefix
InterfaceIPs map[string][]netaddr.IP
InterfaceUp map[string]bool
// HaveV6Global is whether this machine has an IPv6 global address
@@ -237,9 +186,10 @@ func (s *State) String() string {
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
ifs := make([]string, 0, len(s.InterfaceUp))
for k := range s.InterfaceUp {
if anyInterestingIP(s.InterfaceIPs[k]) {
ifs = append(ifs, k)
if allLoopbackIPs(s.InterfaceIPs[k]) {
continue
}
ifs = append(ifs, k)
}
sort.Slice(ifs, func(i, j int) bool {
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
@@ -256,14 +206,14 @@ func (s *State) String() string {
if s.InterfaceUp[ifName] {
fmt.Fprintf(&sb, "%s:[", ifName)
needSpace := false
for _, pfx := range s.InterfaceIPs[ifName] {
if !isInterestingIP(pfx.IP) {
for _, ip := range s.InterfaceIPs[ifName] {
if ip.IsLinkLocalUnicast() {
continue
}
if needSpace {
sb.WriteString(" ")
}
fmt.Fprintf(&sb, "%s", pfx)
fmt.Fprintf(&sb, "%s", ip)
needSpace = true
}
sb.WriteString("]")
@@ -297,60 +247,20 @@ func (s *State) AnyInterfaceUp() bool {
return s != nil && (s.HaveV4 || s.HaveV6Global)
}
// RemoveUninterestingInterfacesAndAddresses removes uninteresting IPs
// from InterfaceIPs, also removing from both the InterfaceIPs and
// InterfaceUp map any interfaces that don't have any interesting IPs.
func (s *State) RemoveUninterestingInterfacesAndAddresses() {
for ifName := range s.InterfaceUp {
ips := s.InterfaceIPs[ifName]
keep := ips[:0]
for _, pfx := range ips {
if isInterestingIP(pfx.IP) {
keep = append(keep, pfx)
}
}
if len(keep) == 0 {
delete(s.InterfaceUp, ifName)
delete(s.InterfaceIPs, ifName)
continue
}
if len(keep) < len(ips) {
s.InterfaceIPs[ifName] = keep
}
}
}
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
// are owned by this process. (TODO: make this true; currently it
// uses some heuristics)
// makes the Linux-only assumption that the interface is named
// /^tailscale/)
func (s *State) RemoveTailscaleInterfaces() {
for name, pfxs := range s.InterfaceIPs {
if isTailscaleInterface(name, pfxs) {
for name := range s.InterfaceIPs {
if isTailscaleInterfaceName(name) {
delete(s.InterfaceIPs, name)
delete(s.InterfaceUp, name)
}
}
}
func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool {
for _, pfx := range pfxs {
if tsaddr.IsTailscaleIP(pfx.IP) {
return true
}
}
return false
}
func isTailscaleInterface(name string, ips []netaddr.IPPrefix) bool {
if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) {
// On macOS in the sandboxed app (at least as of
// 2021-02-25), we often see two utun devices
// (e.g. utun4 and utun7) with the same IPv4 and IPv6
// addresses. Just remove all utun devices with
// Tailscale IPs until we know what's happening with
// macOS NetworkExtensions and utun devices.
return true
}
func isTailscaleInterfaceName(name string) bool {
return name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
}
@@ -363,27 +273,20 @@ var getPAC func() string
// It does not set the returned State.IsExpensive. The caller can populate that.
func GetState() (*State, error) {
s := &State{
InterfaceIPs: make(map[string][]netaddr.IPPrefix),
InterfaceIPs: make(map[string][]netaddr.IP),
InterfaceUp: make(map[string]bool),
}
if err := ForeachInterface(func(ni Interface, pfxs []netaddr.IPPrefix) {
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
ifUp := ni.IsUp()
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
s.InterfaceUp[ni.Name] = ifUp
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
if !ifUp || isTailscaleInterface(ni.Name, pfxs) {
return
}
for _, pfx := range pfxs {
if pfx.IP.IsLoopback() || pfx.IP.IsLinkLocalUnicast() {
continue
}
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP)
s.HaveV4 = s.HaveV4 || pfx.IP.Is4()
if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
s.HaveV4 = s.HaveV4 || ip.Is4()
}
}); err != nil {
return nil, err
}
s.DefaultRouteInterface, _ = DefaultRouteInterface()
if s.AnyInterfaceUp() {
@@ -413,8 +316,7 @@ func HTTPOfListener(ln net.Listener) string {
var goodIP string
var privateIP string
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
ip := pfx.IP
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
if isPrivateIP(ip) {
if privateIP == "" {
privateIP = ip.String()
@@ -450,8 +352,7 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
if !ok {
return
}
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
ip := pfx.IP
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
return
}
@@ -491,23 +392,14 @@ var (
v6Global1 = mustCIDR("2000::/3")
)
// anyInterestingIP reports whether pfxs contains any IP that matches
// isInterestingIP.
func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
for _, pfx := range pfxs {
if isInterestingIP(pfx.IP) {
return true
}
}
return false
}
// isInterestingIP reports whether ip is an interesting IP that we
// should log in interfaces.State logging. We don't need to show
// localhost or link-local addresses.
func isInterestingIP(ip netaddr.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
func allLoopbackIPs(ips []netaddr.IP) bool {
if len(ips) == 0 {
return false
}
for _, ip := range ips {
if !ip.IsLoopback() {
return false
}
}
return true
}

View File

@@ -6,13 +6,9 @@ package interfaces
import (
"errors"
"fmt"
"net"
"os/exec"
"syscall"
"go4.org/mem"
"golang.org/x/net/route"
"inet.af/netaddr"
"tailscale.com/util/lineread"
"tailscale.com/version"
@@ -76,65 +72,3 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
}
var errStopReadingNetstatTable = errors.New("found private gateway")
func DefaultRouteInterface() (string, error) {
idx, err := DefaultRouteInterfaceIndex()
if err != nil {
return "", err
}
iface, err := net.InterfaceByIndex(idx)
if err != nil {
return "", err
}
return iface.Name, nil
}
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
// Internet:
// Destination Gateway Flags Netif Expire
// default 10.0.0.1 UGSc en0 <-- want this one
// default 10.0.0.1 UGScI en1
// From man netstat:
// U RTF_UP Route usable
// G RTF_GATEWAY Destination requires forwarding by intermediary
// S RTF_STATIC Manually added
// c RTF_PRCLONING Protocol-specified generate new routes on use
// I RTF_IFSCOPE Route is associated with an interface scope
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
}
}
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
}

View File

@@ -15,7 +15,7 @@ package interfaces
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
// Otherwise, it returns 0.
uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
{
// sockaddrs are after the message header
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
@@ -38,7 +38,7 @@ uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
return 0; // gateway not IPv4
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
uint32_t ip;
int ip;
ip = gateway_si->sin_addr.s_addr;
unsigned char a, b;
@@ -62,7 +62,7 @@ uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
// If no private gateway IP address was found, it returns 0.
// On an error, it returns an error code in (0, 255].
// Any private gateway IP address is > 255.
uint32_t privateGatewayIP()
int privateGatewayIP()
{
size_t needed;
int mib[6];
@@ -90,7 +90,7 @@ uint32_t privateGatewayIP()
struct rt_msghdr2 *rtm;
for (next = buf; next < lim; next += rtm->rtm_msglen) {
rtm = (struct rt_msghdr2 *)next;
uint32_t ip;
int ip;
ip = privateGatewayIPFromRoute(rtm);
if (ip) {
free(buf);
@@ -105,7 +105,6 @@ import "C"
import (
"encoding/binary"
"log"
"inet.af/netaddr"
)
@@ -117,7 +116,6 @@ func init() {
func likelyHomeRouterIPDarwinSyscall() (ret netaddr.IP, ok bool) {
ip := C.privateGatewayIP()
if ip < 255 {
log.Printf("likelyHomeRouterIPDarwinSyscall: error code %v", ip)
return netaddr.IP{}, false
}
var q [4]byte

View File

@@ -1,17 +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.
// +build linux darwin,!redo
package interfaces
import "testing"
func TestDefaultRouteInterface(t *testing.T) {
v, err := DefaultRouteInterface()
if err != nil {
t.Fatal(err)
}
t.Logf("got %q", v)
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows,!darwin
// +build !linux,!windows
package interfaces

View File

@@ -5,9 +5,30 @@
package interfaces
import (
"net"
"testing"
)
func TestIsTailscaleIP(t *testing.T) {
tests := []struct {
ip string
want bool
}{
{"100.81.251.94", true},
{"8.8.8.8", false},
}
for _, tt := range tests {
ip := net.ParseIP(tt.ip)
if ip == nil {
t.Fatalf("failed to parse IP %q", tt.ip)
}
got := IsTailscaleIP(ip)
if got != tt.want {
t.Errorf("F(%q) = %v; want %v", tt.ip, got, tt.want)
}
}
}
func TestGetState(t *testing.T) {
st, err := GetState()
if err != nil {

View File

@@ -8,7 +8,9 @@ package netcheck
import (
"bufio"
"context"
"crypto/rand"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
@@ -23,11 +25,11 @@ import (
"time"
"github.com/tcnksm/go-httpstat"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/portmapper"
"tailscale.com/net/stun"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -66,6 +68,12 @@ const (
// more aggressive than defaultActiveRetransmitTime. A few extra
// packets at startup is fine.
defaultInitialRetransmitTime = 100 * time.Millisecond
// portMapServiceProbeTimeout is the time we wait for port mapping
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
// decide that they're not there. Since these services are on the
// same LAN as this machine and a single L3 hop away, we don't
// give them much time to respond.
portMapServiceProbeTimeout = 100 * time.Millisecond
)
type Report struct {
@@ -152,10 +160,6 @@ type Client struct {
// It defaults to ":0".
UDPBindAddr string
// PortMapper, if non-nil, is used for portmap queries.
// If nil, portmap discovery is not done.
PortMapper *portmapper.Client // lazily initialized on first use
mu sync.Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
prev map[time.Time]*Report // some previous reports
@@ -215,8 +219,8 @@ func (c *Client) handleHairSTUNLocked(pkt []byte, src netaddr.IPPort) bool {
// (non-incremental) probe of all DERP regions.
func (c *Client) MakeNextReportFull() {
c.mu.Lock()
defer c.mu.Unlock()
c.nextFull = true
c.mu.Unlock()
}
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
@@ -305,9 +309,6 @@ type probePlan map[string][]probe
func sortRegions(dm *tailcfg.DERPMap, last *Report) (prev []*tailcfg.DERPRegion) {
prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, reg := range dm.Regions {
if reg.Avoid {
continue
}
prev = append(prev, reg)
}
sort.Slice(prev, func(i, j int) bool {
@@ -362,7 +363,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
tries = 2
} else if hadBoth {
// For dual stack machines, make the 3rd & slower nodes alternate
// beetween.
// breetween
if ri%2 == 0 {
do4, do6 = true, false
} else {
@@ -373,12 +374,6 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
do6 = false
}
if reg.RegionID == last.PreferredDERP {
// But if we already had a DERP home, try extra hard to
// make sure it's there so we don't flip flop around.
tries = 4
}
for try := 0; try < tries; try++ {
if len(reg.Nodes) == 0 {
// Shouldn't be possible.
@@ -393,9 +388,6 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
prevLatency = defaultActiveRetransmitTime
}
delay := time.Duration(try) * prevLatency
if try > 1 {
delay += time.Duration(try) * 50 * time.Millisecond
}
if do4 {
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
}
@@ -688,20 +680,104 @@ func (rs *reportState) setOptBool(b *opt.Bool, v bool) {
func (rs *reportState) probePortMapServices() {
defer rs.waitPortMap.Done()
gw, myIP, ok := interfaces.LikelyHomeRouterIP()
if !ok {
return
}
rs.setOptBool(&rs.report.UPnP, false)
rs.setOptBool(&rs.report.PMP, false)
rs.setOptBool(&rs.report.PCP, false)
res, err := rs.c.PortMapper.Probe(context.Background())
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
rs.c.logf("[v1] probePortMapServices: me %v -> gw %v", myIP, gw)
// Create a UDP4 socket used just for querying for UPnP, NAT-PMP, and PCP.
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
if err != nil {
rs.c.logf("probePortMapServices: %v", err)
return
}
defer uc.Close()
tempPort := uc.LocalAddr().(*net.UDPAddr).Port
uc.SetReadDeadline(time.Now().Add(portMapServiceProbeTimeout))
rs.setOptBool(&rs.report.UPnP, res.UPnP)
rs.setOptBool(&rs.report.PMP, res.PMP)
rs.setOptBool(&rs.report.PCP, res.PCP)
// Send request packets for all three protocols.
uc.WriteTo(uPnPPacket, port1900)
uc.WriteTo(pmpPacket, port5351)
uc.WriteTo(pcpPacket(myIP, tempPort, false), port5351)
res := make([]byte, 1500)
sentPCPDelete := false
for {
n, addr, err := uc.ReadFrom(res)
if err != nil {
return
}
switch addr.(*net.UDPAddr).Port {
case 1900:
if mem.Contains(mem.B(res[:n]), mem.S(":InternetGatewayDevice:")) {
rs.setOptBool(&rs.report.UPnP, true)
}
case 5351:
if n == 12 && res[0] == 0x00 { // right length and version 0
rs.setOptBool(&rs.report.PMP, true)
}
if n == 60 && res[0] == 0x02 { // right length and version 2
rs.setOptBool(&rs.report.PCP, true)
if !sentPCPDelete {
sentPCPDelete = true
// And now delete the mapping.
// (PCP is the only protocol of the three that requires
// we cause a side effect to detect whether it's present,
// so we need to redo that side effect now.)
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
}
}
}
}
}
var pmpPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
var v4unspec, _ = netaddr.ParseIP("0.0.0.0")
// pcpPacket generates a PCP packet with a MAP opcode.
func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
const udpProtoNumber = 17
lifetimeSeconds := uint32(1)
if delete {
lifetimeSeconds = 0
}
const opMap = 1
// 24 byte header + 36 byte map opcode
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
pkt[0] = 2 // version
pkt[1] = opMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mappping nonce
mapOp[12] = udpProtoNumber
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
v4unspec16 := v4unspec.As16()
copy(mapOp[20:], v4unspec16[:])
return pkt
}
func newReport() *Report {
@@ -778,7 +854,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}
defer rs.pc4Hair.Close()
if !c.SkipExternalNetwork && c.PortMapper != nil {
if !c.SkipExternalNetwork {
rs.waitPortMap.Add(1)
go rs.probePortMapServices()
}
@@ -851,7 +927,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
rs.waitHairCheck(ctx)
c.vlogf("hairCheck done")
if !c.SkipExternalNetwork && c.PortMapper != nil {
if !c.SkipExternalNetwork {
rs.waitPortMap.Wait()
c.vlogf("portMap done")
}

View File

@@ -410,35 +410,6 @@ func TestMakeProbePlan(t *testing.T) {
"region-5-v6": []probe{p("5a", 6), p("5b", 6, 100*ms), p("5c", 6, 200*ms)},
},
},
{
name: "try_harder_for_preferred_derp",
dm: basicMap,
have6if: true,
last: &Report{
RegionLatency: map[int]time.Duration{
1: 10 * time.Millisecond,
2: 20 * time.Millisecond,
3: 30 * time.Millisecond,
4: 40 * time.Millisecond,
},
RegionV4Latency: map[int]time.Duration{
1: 10 * time.Millisecond,
2: 20 * time.Millisecond,
},
RegionV6Latency: map[int]time.Duration{
3: 30 * time.Millisecond,
4: 40 * time.Millisecond,
},
PreferredDERP: 1,
},
want: probePlan{
"region-1-v4": []probe{p("1a", 4), p("1a", 4, 12*ms), p("1a", 4, 124*ms), p("1a", 4, 186*ms)},
"region-1-v6": []probe{p("1a", 6), p("1a", 6, 12*ms), p("1a", 6, 124*ms), p("1a", 6, 186*ms)},
"region-2-v4": []probe{p("2a", 4), p("2b", 4, 24*ms)},
"region-2-v6": []probe{p("2a", 6), p("2b", 6, 24*ms)},
"region-3-v4": []probe{p("3a", 4)},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -1,52 +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.
// +build darwin,!redo
package netns
import (
"fmt"
"log"
"strings"
"syscall"
"golang.org/x/sys/unix"
"tailscale.com/net/interfaces"
)
// control marks c as necessary to dial in a separate network namespace.
//
// It's intentionally the same signature as net.Dialer.Control
// and net.ListenConfig.Control.
func control(network, address string, c syscall.RawConn) error {
if strings.HasPrefix(address, "127.") || address == "::1" {
// Don't bind to an interface for localhost connections.
return nil
}
idx, err := interfaces.DefaultRouteInterfaceIndex()
if err != nil {
log.Printf("netns: DefaultRouteInterfaceIndex: %v", err)
return nil
}
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
proto := unix.IPPROTO_IP
opt := unix.IP_BOUND_IF
if v6 {
proto = unix.IPPROTO_IPV6
opt = unix.IPV6_BOUND_IF
}
var sockErr error
err = c.Control(func(fd uintptr) {
sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx)
})
if err != nil {
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
}
if sockErr != nil {
log.Printf("netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr)
}
return sockErr
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows,!darwin darwin,redo
// +build !linux,!windows
package netns

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package netns contains the common code for using the Go net package
// in a logical "network namespace" to avoid routing loops where
// Tailscale-created packets would otherwise loop back through
// Tailscale routes.
//
// Despite the name netns, the exact mechanism used differs by
// operating system, and perhaps even by version of the OS.
//
// The netns package also handles connecting via SOCKS proxies when
// configured by the environment.
package netns
import (
"flag"
"testing"
)
var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests")
func TestDial(t *testing.T) {
if !*extNetwork {
t.Skip("skipping test without --use-external-network")
}
d := NewDialer()
c, err := d.Dial("tcp", "google.com:80")
if err != nil {
t.Fatal(err)
}
defer c.Close()
t.Logf("got addr %v", c.RemoteAddr())
c, err = d.Dial("tcp4", "google.com:80")
if err != nil {
t.Fatal(err)
}
defer c.Close()
t.Logf("got addr %v", c.RemoteAddr())
}

View File

@@ -116,12 +116,6 @@ func (q *Parsed) Decode(b []byte) {
}
}
// StuffForTesting makes Parsed contain a len-bytes buffer. Used in
// tests to build up a synthetic parse result with a non-zero buffer.
func (q *Parsed) StuffForTesting(len int) {
q.b = make([]byte, len)
}
func (q *Parsed) decode4(b []byte) {
if len(b) < ip4HeaderLength {
q.IPVersion = 0

View File

@@ -23,14 +23,12 @@ import (
// Tailscale node has rejected the connection from another. Unlike a
// TCP RST, this includes a reason.
//
// On the wire, after the IP header, it's currently 7 or 8 bytes:
// On the wire, after the IP header, it's currently 7 bytes:
// * '!'
// * IPProto byte (IANA protocol number: TCP or UDP)
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
// * srcPort big endian uint16
// * dstPort big endian uint16
// * [optional] byte of flag bits:
// lowest bit (0x1): MaybeBroken
//
// In the future it might also accept 16 byte IP flow src/dst IPs
// after the header, if they're different than the IP-level ones.
@@ -41,21 +39,8 @@ type TailscaleRejectedHeader struct {
Dst netaddr.IPPort // rejected flow's dst
Proto IPProto // proto that was rejected (TCP or UDP)
Reason TailscaleRejectReason // why the connection was rejected
// MaybeBroken is whether the rejection is non-terminal (the
// client should not fail immediately). This is sent by a
// target when it's not sure whether it's totally broken, but
// it might be. For example, the target tailscaled might think
// its host firewall or IP forwarding aren't configured
// properly, but tailscaled might be wrong (not having enough
// visibility into what the OS is doing). When true, the
// message is simply an FYI as a potential reason to use for
// later when the pendopen connection tracking timer expires.
MaybeBroken bool
}
const rejectFlagBitMaybeBroken = 0x1
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
}
@@ -67,32 +52,14 @@ func (rh TailscaleRejectedHeader) String() string {
type TSMPType uint8
const (
// TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader.
TSMPTypeRejectedConn TSMPType = '!'
)
type TailscaleRejectReason byte
// IsZero reports whether r is the zero value, representing no rejection.
func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone }
const (
// TailscaleRejectReasonNone is the TailscaleRejectReason zero value.
TailscaleRejectReasonNone TailscaleRejectReason = 0
// RejectedDueToACLs means that the host rejected the connection due to ACLs.
RejectedDueToACLs TailscaleRejectReason = 'A'
// RejectedDueToShieldsUp means that the host rejected the connection due to shields being up.
RejectedDueToACLs TailscaleRejectReason = 'A'
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
// RejectedDueToIPForwarding means that the relay node's IP
// forwarding is disabled.
RejectedDueToIPForwarding TailscaleRejectReason = 'F'
// RejectedDueToHostFirewall means that the target host's
// firewall is blocking the traffic.
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
)
func (r TailscaleRejectReason) String() string {
@@ -101,32 +68,22 @@ func (r TailscaleRejectReason) String() string {
return "acl"
case RejectedDueToShieldsUp:
return "shields"
case RejectedDueToIPForwarding:
return "host-ip-forwarding-unavailable"
case RejectedDueToHostFirewall:
return "host-firewall"
}
return fmt.Sprintf("0x%02x", byte(r))
}
func (h TailscaleRejectedHeader) hasFlags() bool {
return h.MaybeBroken // the only one currently
}
func (h TailscaleRejectedHeader) Len() int {
v := 1 + // TSMPType byte
var ipHeaderLen int
if h.IPSrc.Is4() {
ipHeaderLen = ip4HeaderLength
} else if h.IPSrc.Is6() {
ipHeaderLen = ip6HeaderLength
}
return ipHeaderLen +
1 + // TSMPType byte
1 + // IPProto byte
1 + // TailscaleRejectReason byte
2*2 // 2 uint16 ports
if h.IPSrc.Is4() {
v += ip4HeaderLength
} else if h.IPSrc.Is6() {
v += ip6HeaderLength
}
if h.hasFlags() {
v++
}
return v
}
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
@@ -160,14 +117,6 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
buf[2] = byte(h.Reason)
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
if h.hasFlags() {
var flags byte
if h.MaybeBroken {
flags |= rejectFlagBitMaybeBroken
}
buf[7] = flags
}
return nil
}
@@ -180,17 +129,12 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
return
}
h = TailscaleRejectedHeader{
return TailscaleRejectedHeader{
Proto: IPProto(p[1]),
Reason: TailscaleRejectReason(p[2]),
IPSrc: pp.Src.IP,
IPDst: pp.Dst.IP,
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
}
if len(p) > 7 {
flags := p[7]
h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0
}
return h, true
}, true
}

View File

@@ -37,18 +37,6 @@ func TestTailscaleRejectedHeader(t *testing.T) {
},
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
},
{
h: TailscaleRejectedHeader{
IPSrc: netaddr.MustParseIP("2::2"),
IPDst: netaddr.MustParseIP("1::1"),
Src: netaddr.MustParseIPPort("[1::1]:567"),
Dst: netaddr.MustParseIPPort("[2::2]:443"),
Proto: UDP,
Reason: RejectedDueToIPForwarding,
MaybeBroken: true,
},
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: host-ip-forwarding-unavailable",
},
}
for i, tt := range tests {
gotStr := tt.h.String()

View File

@@ -1,618 +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.
// Package portmapper is a UDP port mapping client. It currently only does
// NAT-PMP, but will likely do UPnP and perhaps PCP later.
package portmapper
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
// References:
//
// NAT-PMP: https://tools.ietf.org/html/rfc6886
// PCP: https://tools.ietf.org/html/rfc6887
// portMapServiceTimeout is the time we wait for port mapping
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
// decide that they're not there. Since these services are on the
// same LAN as this machine and a single L3 hop away, we don't
// give them much time to respond.
const portMapServiceTimeout = 250 * time.Millisecond
// trustServiceStillAvailableDuration is how often we re-verify a port
// mapping service is available.
const trustServiceStillAvailableDuration = 10 * time.Minute
// Client is a port mapping client.
type Client struct {
logf logger.Logf
mu sync.Mutex // guards following, and all fields thereof
lastMyIP netaddr.IP
lastGW netaddr.IP
closed bool
lastProbe time.Time
pmpPubIP netaddr.IP // non-zero if known
pmpPubIPTime time.Time // time pmpPubIP last verified
pmpLastEpoch uint32
pcpSawTime time.Time // time we last saw PCP was available
uPnPSawTime time.Time // time we last saw UPnP was available
localPort uint16
pmpMapping *pmpMapping // non-nil if we have a PMP mapping
}
// HaveMapping reports whether we have a current valid mapping.
func (c *Client) HaveMapping() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.pmpMapping != nil && c.pmpMapping.useUntil.After(time.Now())
}
// pmpMapping is an already-created PMP mapping.
//
// All fields are immutable once created.
type pmpMapping struct {
gw netaddr.IP
external netaddr.IPPort
internal netaddr.IPPort
useUntil time.Time // the mapping's lifetime minus renewal interval
epoch uint32
}
// externalValid reports whether m.external is valid, with both its IP and Port populated.
func (m *pmpMapping) externalValid() bool {
return !m.external.IP.IsZero() && m.external.Port != 0
}
// release does a best effort fire-and-forget release of the PMP mapping m.
func (m *pmpMapping) release() {
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
pkt := buildPMPRequestMappingPacket(m.internal.Port, m.external.Port, pmpMapLifetimeDelete)
uc.WriteTo(pkt, netaddr.IPPort{IP: m.gw, Port: pmpPort}.UDPAddr())
}
// NewClient returns a new portmapping client.
func NewClient(logf logger.Logf) *Client {
return &Client{
logf: logf,
}
}
// NoteNetworkDown should be called when the network has transitioned to a down state.
// It's too late to release port mappings at this point (the user might've just turned off
// their wifi), but we can make sure we invalidate mappings for later when the network
// comes back.
func (c *Client) NoteNetworkDown() {
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateMappingsLocked(false)
}
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
c.closed = true
c.invalidateMappingsLocked(true)
// TODO: close some future ever-listening UDP socket(s),
// waiting for multicast announcements from router.
return nil
}
// SetLocalPort updates the local port number to which we want to port
// map UDP traffic.
func (c *Client) SetLocalPort(localPort uint16) {
c.mu.Lock()
defer c.mu.Unlock()
if c.localPort == localPort {
return
}
c.localPort = localPort
c.invalidateMappingsLocked(true)
}
func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
gw, myIP, ok = interfaces.LikelyHomeRouterIP()
if !ok {
gw = netaddr.IP{}
myIP = netaddr.IP{}
}
c.mu.Lock()
defer c.mu.Unlock()
if gw != c.lastGW || myIP != c.lastMyIP || !ok {
c.lastMyIP = myIP
c.lastGW = gw
c.invalidateMappingsLocked(true)
}
return
}
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
if c.pmpMapping != nil {
if releaseOld {
c.pmpMapping.release()
}
c.pmpMapping = nil
}
c.pmpPubIP = netaddr.IP{}
c.pmpPubIPTime = time.Time{}
c.pcpSawTime = time.Time{}
c.uPnPSawTime = time.Time{}
}
func (c *Client) sawPMPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.sawPMPRecentlyLocked()
}
func (c *Client) sawPMPRecentlyLocked() bool {
return !c.pmpPubIP.IsZero() && c.pmpPubIPTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
func (c *Client) sawPCPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
func (c *Client) sawUPnPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
// closeCloserOnContextDone starts a new goroutine to call c.Close
// if/when ctx becomes done.
// To stop the goroutine, call the returned stop func.
func closeCloserOnContextDone(ctx context.Context, c io.Closer) (stop func()) {
// Close uc on ctx being done.
ctxDone := ctx.Done()
if ctxDone == nil {
return func() {}
}
stopWaitDone := make(chan struct{})
go func() {
select {
case <-stopWaitDone:
case <-ctxDone:
c.Close()
}
}()
return func() { close(stopWaitDone) }
}
// NoMappingError is returned by CreateOrGetMapping when no NAT
// mapping could be returned.
type NoMappingError struct {
err error
}
func (nme NoMappingError) Unwrap() error { return nme.err }
func (nme NoMappingError) Error() string { return fmt.Sprintf("no NAT mapping available: %v", nme.err) }
// IsNoMappingError reports whether err is of type NoMappingError.
func IsNoMappingError(err error) bool {
_, ok := err.(NoMappingError)
return ok
}
var (
ErrNoPortMappingServices = errors.New("no port mapping services were found")
ErrGatewayNotFound = errors.New("failed to look up gateway address")
)
// CreateOrGetMapping either creates a new mapping or returns a cached
// valid one.
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPort, err error) {
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return netaddr.IPPort{}, NoMappingError{ErrGatewayNotFound}
}
c.mu.Lock()
localPort := c.localPort
m := &pmpMapping{
gw: gw,
internal: netaddr.IPPort{IP: myIP, Port: localPort},
}
// prevPort is the port we had most previously, if any. We try
// to ask for the same port. 0 means to give us any port.
var prevPort uint16
// Do we have an existing mapping that's valid?
now := time.Now()
if m := c.pmpMapping; m != nil {
if now.Before(m.useUntil) {
defer c.mu.Unlock()
return m.external, nil
}
// The mapping might still be valid, so just try to renew it.
prevPort = m.external.Port
}
// If we just did a Probe (e.g. via netchecker) but didn't
// find a PMP service, bail out early rather than probing
// again. Cuts down latency for most clients.
haveRecentPMP := c.sawPMPRecentlyLocked()
if haveRecentPMP {
m.external.IP = c.pmpPubIP
}
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
c.mu.Unlock()
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
c.mu.Unlock()
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
if err != nil {
return netaddr.IPPort{}, err
}
defer uc.Close()
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
defer closeCloserOnContextDone(ctx, uc)()
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}
pmpAddru := pmpAddr.UDPAddr()
// Ask for our external address if needed.
if m.external.IP.IsZero() {
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
return netaddr.IPPort{}, err
}
}
// And ask for a mapping.
pmpReqMapping := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
if _, err := uc.WriteTo(pmpReqMapping, pmpAddru); err != nil {
return netaddr.IPPort{}, err
}
res := make([]byte, 1500)
for {
n, srci, err := uc.ReadFrom(res)
if err != nil {
if ctx.Err() == context.Canceled {
return netaddr.IPPort{}, err
}
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
srcu := srci.(*net.UDPAddr)
src, ok := netaddr.FromStdAddr(srcu.IP, srcu.Port, srcu.Zone)
if !ok {
continue
}
if src == pmpAddr {
pres, ok := parsePMPResponse(res[:n])
if !ok {
c.logf("unexpected PMP response: % 02x", res[:n])
continue
}
if pres.ResultCode != 0 {
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external.IP = pres.PublicAddr
}
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
m.external.Port = pres.ExternalPort
d := time.Duration(pres.MappingValidSeconds) * time.Second
d /= 2 // renew in half the time
m.useUntil = time.Now().Add(d)
m.epoch = pres.SecondsSinceEpoch
}
}
if m.externalValid() {
c.mu.Lock()
defer c.mu.Unlock()
c.pmpMapping = m
return m.external, nil
}
}
}
type pmpResultCode uint16
// NAT-PMP constants.
const (
pmpPort = 5351
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
pmpOpMapPublicAddr = 0
pmpOpMapUDP = 1
pmpOpReply = 0x80 // OR'd into request's op code on response
pmpCodeOK pmpResultCode = 0
pmpCodeUnsupportedVersion pmpResultCode = 1
pmpCodeNotAuthorized pmpResultCode = 2 // "e.g., box supports mapping, but user has turned feature off"
pmpCodeNetworkFailure pmpResultCode = 3 // "e.g., NAT box itself has not obtained a DHCP lease"
pmpCodeOutOfResources pmpResultCode = 4
pmpCodeUnsupportedOpcode pmpResultCode = 5
)
func buildPMPRequestMappingPacket(localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
pkt = make([]byte, 12)
pkt[1] = pmpOpMapUDP
binary.BigEndian.PutUint16(pkt[4:], localPort)
binary.BigEndian.PutUint16(pkt[6:], prevPort)
binary.BigEndian.PutUint32(pkt[8:], lifetimeSec)
return pkt
}
type pmpResponse struct {
OpCode uint8
ResultCode pmpResultCode
SecondsSinceEpoch uint32
// For Map ops:
MappingValidSeconds uint32
InternalPort uint16
ExternalPort uint16
// For public addr ops:
PublicAddr netaddr.IP
}
func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
if len(pkt) < 12 {
return
}
ver := pkt[0]
if ver != 0 {
return
}
res.OpCode = pkt[1]
res.ResultCode = pmpResultCode(binary.BigEndian.Uint16(pkt[2:]))
res.SecondsSinceEpoch = binary.BigEndian.Uint32(pkt[4:])
if res.OpCode == pmpOpReply|pmpOpMapUDP {
if len(pkt) != 16 {
return res, false
}
res.InternalPort = binary.BigEndian.Uint16(pkt[8:])
res.ExternalPort = binary.BigEndian.Uint16(pkt[10:])
res.MappingValidSeconds = binary.BigEndian.Uint32(pkt[12:])
}
if res.OpCode == pmpOpReply|pmpOpMapPublicAddr {
if len(pkt) != 12 {
return res, false
}
res.PublicAddr = netaddr.IPv4(pkt[8], pkt[9], pkt[10], pkt[11])
}
return res, true
}
type ProbeResult struct {
PCP bool
PMP bool
UPnP bool
}
// Probe returns a summary of which port mapping services are
// available on the network.
//
// If a probe has run recently and there haven't been any network changes since,
// the returned result might be server from the Client's cache, without
// sending any network traffic.
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return res, ErrGatewayNotFound
}
defer func() {
if err == nil {
c.mu.Lock()
defer c.mu.Unlock()
c.lastProbe = time.Now()
}
}()
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
if err != nil {
c.logf("ProbePCP: %v", err)
return res, err
}
defer uc.Close()
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
defer closeCloserOnContextDone(ctx, uc)()
pcpAddr := netaddr.IPPort{IP: gw, Port: pcpPort}.UDPAddr()
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}.UDPAddr()
upnpAddr := netaddr.IPPort{IP: gw, Port: upnpPort}.UDPAddr()
// Don't send probes to services that we recently learned (for
// the same gw/myIP) are available. See
// https://github.com/tailscale/tailscale/issues/1001
if c.sawPMPRecently() {
res.PMP = true
} else {
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else {
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
}
if c.sawUPnPRecently() {
res.UPnP = true
} else {
uc.WriteTo(uPnPPacket, upnpAddr)
}
buf := make([]byte, 1500)
for {
if res.PCP && res.PMP && res.UPnP {
// Nothing more to discover.
return res, nil
}
n, addr, err := uc.ReadFrom(buf)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
err = nil
}
return res, err
}
port := addr.(*net.UDPAddr).Port
switch port {
case upnpPort:
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
res.UPnP = true
c.mu.Lock()
c.uPnPSawTime = time.Now()
c.mu.Unlock()
}
case pcpPort: // same as pmpPort
if pres, ok := parsePCPResponse(buf[:n]); ok {
if pres.OpCode == pcpOpReply|pcpOpAnnounce && pres.ResultCode == pcpCodeOK {
c.logf("Got PCP response: epoch: %v", pres.Epoch)
res.PCP = true
c.mu.Lock()
c.pcpSawTime = time.Now()
c.mu.Unlock()
continue
}
c.logf("unexpected PCP probe response: %+v", pres)
}
if pres, ok := parsePMPResponse(buf[:n]); ok {
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
res.PMP = true
c.mu.Lock()
c.pmpPubIP = pres.PublicAddr
c.pmpPubIPTime = time.Now()
c.pmpLastEpoch = pres.SecondsSinceEpoch
c.mu.Unlock()
continue
}
c.logf("unexpected PMP probe response: %+v", pres)
}
}
}
}
const (
pcpVersion = 2
pcpPort = 5351
pcpCodeOK = 0
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
)
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
func pcpAnnounceRequest(myIP netaddr.IP) []byte {
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make([]byte, 24)
pkt[0] = pcpVersion // version
pkt[1] = pcpOpAnnounce
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
return pkt
}
//lint:ignore U1000 moved this code from netcheck's old PCP probing; will be needed when we add PCP mapping
// pcpMapRequest generates a PCP packet with a MAP opcode.
func pcpMapRequest(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
const udpProtoNumber = 17
lifetimeSeconds := uint32(1)
if delete {
lifetimeSeconds = 0
}
const opMap = 1
// 24 byte header + 36 byte map opcode
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
pkt[0] = 2 // version
pkt[1] = opMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mappping nonce
mapOp[12] = udpProtoNumber
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
v4unspec := netaddr.MustParseIP("0.0.0.0")
v4unspec16 := v4unspec.As16()
copy(mapOp[20:], v4unspec16[:])
return pkt
}
type pcpResponse struct {
OpCode uint8
ResultCode uint8
Lifetime uint32
Epoch uint32
}
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
if len(b) < 24 || b[0] != pcpVersion {
return
}
res.OpCode = b[1]
res.ResultCode = b[3]
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true
}
const (
upnpPort = 1900
)
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"

View File

@@ -1,54 +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.
package portmapper
import (
"context"
"os"
"strconv"
"testing"
"time"
)
func TestCreateOrGetMapping(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(t.Logf)
c.SetLocalPort(1234)
for i := 0; i < 2; i++ {
if i > 0 {
time.Sleep(100 * time.Millisecond)
}
ext, err := c.CreateOrGetMapping(context.Background())
t.Logf("Got: %v, %v", ext, err)
}
}
func TestClientProbe(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(t.Logf)
for i := 0; i < 2; i++ {
if i > 0 {
time.Sleep(100 * time.Millisecond)
}
res, err := c.Probe(context.Background())
t.Logf("Got: %+v, %v", res, err)
}
}
func TestClientProbeThenMap(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(t.Logf)
c.SetLocalPort(1234)
res, err := c.Probe(context.Background())
t.Logf("Probe: %+v, %v", res, err)
ext, err := c.CreateOrGetMapping(context.Background())
t.Logf("CreateOrGetMapping: %v, %v", ext, err)
}

View File

@@ -1,364 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package socks5 is a SOCKS5 server implementation
// for userspace networking in Tailscale.
package socks5
import (
"context"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"strconv"
"time"
"tailscale.com/types/logger"
)
const (
noAuthRequired byte = 0
noAcceptableAuth byte = 255
// socks5Version is the byte that represents the SOCKS version
// in requests.
socks5Version byte = 5
)
// commandType are the bytes sent in SOCKS5 packets
// that represent the kind of connection the client needs.
type commandType byte
// The set of valid SOCKS5 commans as described in RFC 1928.
const (
connect commandType = 1
bind commandType = 2
udpAssociate commandType = 3
)
// addrType are the bytes sent in SOCKS5 packets
// that represent particular address types.
type addrType byte
// The set of valid SOCKS5 address types as defined in RFC 1928.
const (
ipv4 addrType = 1
domainName addrType = 3
ipv6 addrType = 4
)
// replyCode are the bytes sent in SOCKS5 packets
// that represent replies from the server to a client
// request.
type replyCode byte
// The set of valid SOCKS5 reply types as per the RFC 1928.
const (
success replyCode = 0
generalFailure replyCode = 1
connectionNotAllowed replyCode = 2
networkUnreachable replyCode = 3
hostUnreachable replyCode = 4
connectionRefused replyCode = 5
ttlExpired replyCode = 6
commandNotSupported replyCode = 7
addrTypeNotSupported replyCode = 8
)
// Server is a SOCKS5 proxy server.
type Server struct {
// Logf optionally specifies the logger to use.
// If nil, the standard logger is used.
Logf logger.Logf
// Dialer optionally specifies the dialer to use for outgoing connections.
// If nil, the net package's standard dialer is used.
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
}
func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) {
dial := s.Dialer
if dial == nil {
dialer := &net.Dialer{}
dial = dialer.DialContext
}
return dial(ctx, network, addr)
}
func (s *Server) logf(format string, args ...interface{}) {
logf := s.Logf
if logf == nil {
logf = log.Printf
}
logf(format, args...)
}
// Serve accepts and handles incoming connections on the given listener.
func (s *Server) Serve(l net.Listener) error {
defer l.Close()
for {
c, err := l.Accept()
if err != nil {
return err
}
go func() {
conn := &Conn{clientConn: c, srv: s}
err := conn.Run()
if err != nil {
s.logf("client connection failed: %v", err)
conn.clientConn.Close()
}
}()
}
}
// Conn is a SOCKS5 connection for client to reach
// server.
type Conn struct {
// The struct is filled by each of the internal
// methods in turn as the transaction progresses.
srv *Server
clientConn net.Conn
request *request
}
// Run starts the new connection.
func (c *Conn) Run() error {
err := parseClientGreeting(c.clientConn)
if err != nil {
c.clientConn.Write([]byte{socks5Version, noAcceptableAuth})
return err
}
c.clientConn.Write([]byte{socks5Version, noAuthRequired})
return c.handleRequest()
}
func (c *Conn) handleRequest() error {
req, err := parseClientRequest(c.clientConn)
if err != nil {
res := &response{reply: generalFailure}
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
if req.command != connect {
res := &response{reply: commandNotSupported}
buf, _ := res.marshal()
c.clientConn.Write(buf)
return fmt.Errorf("unsupported command %v", req.command)
}
c.request = req
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv, err := c.srv.dial(
ctx,
"tcp",
net.JoinHostPort(c.request.destination, strconv.Itoa(int(c.request.port))),
)
if err != nil {
res := &response{reply: generalFailure}
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
defer srv.Close()
serverAddr, serverPortStr, err := net.SplitHostPort(srv.LocalAddr().String())
if err != nil {
return err
}
serverPort, _ := strconv.Atoi(serverPortStr)
var bindAddrType addrType
if ip := net.ParseIP(serverAddr); ip != nil {
if ip.To4() != nil {
bindAddrType = ipv4
} else {
bindAddrType = ipv6
}
} else {
bindAddrType = domainName
}
res := &response{
reply: success,
bindAddrType: bindAddrType,
bindAddr: serverAddr,
bindPort: uint16(serverPort),
}
buf, err := res.marshal()
if err != nil {
res = &response{reply: generalFailure}
buf, _ = res.marshal()
}
c.clientConn.Write(buf)
errc := make(chan error, 2)
go func() {
_, err := io.Copy(c.clientConn, srv)
if err != nil {
err = fmt.Errorf("from backend to client: %w", err)
}
errc <- err
}()
go func() {
_, err := io.Copy(srv, c.clientConn)
if err != nil {
err = fmt.Errorf("from client to backend: %w", err)
}
errc <- err
}()
return <-errc
}
// parseClientGreeting parses a request initiation packet
// and returns a slice that contains the acceptable auth methods
// for the client.
func parseClientGreeting(r io.Reader) error {
var hdr [2]byte
_, err := io.ReadFull(r, hdr[:])
if err != nil {
return fmt.Errorf("could not read packet header")
}
if hdr[0] != socks5Version {
return fmt.Errorf("incompatible SOCKS version")
}
count := int(hdr[1])
methods := make([]byte, count)
_, err = io.ReadFull(r, methods)
if err != nil {
return fmt.Errorf("could not read methods")
}
for _, m := range methods {
if m == noAuthRequired {
return nil
}
}
return fmt.Errorf("no acceptable auth methods")
}
// request represents data contained within a SOCKS5
// connection request packet.
type request struct {
command commandType
destination string
port uint16
destAddrType addrType
}
// parseClientRequest converts raw packet bytes into a
// SOCKS5Request struct.
func parseClientRequest(r io.Reader) (*request, error) {
var hdr [4]byte
_, err := io.ReadFull(r, hdr[:])
if err != nil {
return nil, fmt.Errorf("could not read packet header")
}
cmd := hdr[1]
destAddrType := addrType(hdr[3])
var destination string
var port uint16
if destAddrType == ipv4 {
var ip [4]byte
_, err = io.ReadFull(r, ip[:])
if err != nil {
return nil, fmt.Errorf("could not read IPv4 address")
}
destination = net.IP(ip[:]).String()
} else if destAddrType == domainName {
var dstSizeByte [1]byte
_, err = io.ReadFull(r, dstSizeByte[:])
if err != nil {
return nil, fmt.Errorf("could not read domain name size")
}
dstSize := int(dstSizeByte[0])
domainName := make([]byte, dstSize)
_, err = io.ReadFull(r, domainName)
if err != nil {
return nil, fmt.Errorf("could not read domain name")
}
destination = string(domainName)
} else if destAddrType == ipv6 {
var ip [16]byte
_, err = io.ReadFull(r, ip[:])
if err != nil {
return nil, fmt.Errorf("could not read IPv6 address")
}
destination = net.IP(ip[:]).String()
} else {
return nil, fmt.Errorf("unsupported address type")
}
var portBytes [2]byte
_, err = io.ReadFull(r, portBytes[:])
if err != nil {
return nil, fmt.Errorf("could not read port")
}
port = binary.BigEndian.Uint16(portBytes[:])
return &request{
command: commandType(cmd),
destination: destination,
port: port,
destAddrType: destAddrType,
}, nil
}
// response contains the contents of
// a response packet sent from the proxy
// to the client.
type response struct {
reply replyCode
bindAddrType addrType
bindAddr string
bindPort uint16
}
// marshal converts a SOCKS5Response struct into
// a packet. If res.reply == Success, it may throw an error on
// receiving an invalid bind address. Otherwise, it will not throw.
func (res *response) marshal() ([]byte, error) {
pkt := make([]byte, 4)
pkt[0] = socks5Version
pkt[1] = byte(res.reply)
pkt[2] = 0 // null reserved byte
pkt[3] = byte(res.bindAddrType)
if res.reply != success {
return pkt, nil
}
var addr []byte
switch res.bindAddrType {
case ipv4:
addr = net.ParseIP(res.bindAddr).To4()
if addr == nil {
return nil, fmt.Errorf("invalid IPv4 address for binding")
}
case domainName:
if len(res.bindAddr) > 255 {
return nil, fmt.Errorf("invalid domain name for binding")
}
addr = make([]byte, 0, len(res.bindAddr)+1)
addr = append(addr, byte(len(res.bindAddr)))
addr = append(addr, []byte(res.bindAddr)...)
case ipv6:
addr = net.ParseIP(res.bindAddr).To16()
if addr == nil {
return nil, fmt.Errorf("invalid IPv6 address for binding")
}
default:
return nil, fmt.Errorf("unsupported address type")
}
pkt = append(pkt, addr...)
port := make([]byte, 2)
binary.BigEndian.PutUint16(port, uint16(res.bindPort))
pkt = append(pkt, port...)
return pkt, nil
}

View File

@@ -59,7 +59,6 @@ func runSTUN(t *testing.T, pc net.PacketConn, stats *stunStats, done chan<- stru
for {
n, addr, err := pc.ReadFrom(buf[:])
if err != nil {
// TODO: when we switch to Go 1.16, replace this with errors.Is(err, net.ErrClosed)
if strings.Contains(err.Error(), "closed network connection") {
t.Logf("STUN server shutdown")
return

View File

@@ -71,17 +71,6 @@ func Tailscale4To6Range() netaddr.IPPrefix {
return ula4To6Range.v
}
// Tailscale4To6Placeholder returns an IP address that can be used as
// a source IP when one is required, but a netmap didn't provide
// any. This address never gets allocated by the 4-to-6 algorithm in
// control.
//
// Currently used to work around a Windows limitation when programming
// IPv6 routes in corner cases.
func Tailscale4To6Placeholder() netaddr.IP {
return Tailscale4To6Range().IP
}
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
// given Tailscale IPv4 address. Returns a zero IP if ipv4 isn't a
// Tailscale IPv4 address.

Some files were not shown because too many files have changed in this diff Show More