Compare commits

..

1 Commits

Author SHA1 Message Date
Brad Fitzpatrick
4c7ee0c9f9 net/dns: make exit node DNS ask OSConfigurator for backup resolvers
Updates #1713

Change-Id: I7be9dab2b2c03749b4c2d99f9f45c11422ac915a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 10:05:23 -08:00
128 changed files with 1357 additions and 5838 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1 @@
go.mod filter=go-mod
*.go diff=golang

View File

@@ -2,20 +2,15 @@
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
version: 2
updates:
## Disabled between releases. We reenable it briefly after every
## stable release, pull in all changes, and close it again so that
## the tree remains more stable during development and the upstream
## changes have time to soak before the next release.
# - package-ecosystem: "gomod"
# directory: "/"
# schedule:
# interval: "daily"
# commit-message:
# prefix: "go.mod:"
# open-pull-requests-limit: 100
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "go.mod:"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
commit-message:
prefix: ".github:"

View File

@@ -19,7 +19,7 @@ jobs:
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v1
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts

View File

@@ -30,11 +30,3 @@ check: staticcheck vet depaware buildwindows build386 buildlinuxarm
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
spk:
go run github.com/tailscale/tailscale-synology@main --version=build -o tailscale.spk --source=.
pushspk: spk
echo "Pushing SPKG to root@${SYNOHOST} (env var SYNOHOST) ..."
scp tailscale.spk root@${SYNOHOST}:
ssh root@${SYNOHOST} /usr/syno/bin/synopkg install tailscale.spk

View File

@@ -8,12 +8,11 @@ Private WireGuard® networks made easy
This repository contains all the open source Tailscale client code and
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
daemon runs primarily on Linux; it also works to varying degrees on
FreeBSD, OpenBSD, Darwin, and Windows.
The Android app is at https://github.com/tailscale/tailscale-android
The Synology package is at https://github.com/tailscale/tailscale-synology
## Using
We serve packages for a variety of distros at

View File

@@ -30,6 +30,6 @@ go run github.com/tailscale/mkctr@latest \
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--tags="v${VERSION_SHORT},v${VERSION_MINOR}" \
--tags="${VERSION_SHORT},${VERSION_MINOR}" \
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
--push

View File

@@ -38,9 +38,6 @@ var (
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
TailscaledSocket = paths.DefaultTailscaledSocket()
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
TailscaledSocketSetExplicitly bool
// TailscaledDialer is the DialContext func that connects to the local machine's
// tailscaled or equivalent.
TailscaledDialer = defaultDialer
@@ -50,8 +47,7 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
// TODO: make this part of a safesocket.ConnectionStrategy
if !TailscaledSocketSetExplicitly {
if TailscaledSocket == paths.DefaultTailscaledSocket() {
// 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.
@@ -60,11 +56,7 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
// The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided.
s.UseFallback(false)
return safesocket.Connect(s)
return safesocket.Connect(TailscaledSocket, safesocket.WindowsLocalPort)
}
var (
@@ -98,27 +90,6 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
return tsClient.Do(req)
}
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
path := req.URL.Path
pathPrefix := path
if i := strings.Index(path, "?"); i != -1 {
pathPrefix = path[:i]
}
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
type errorJSON struct {
Error string
}
@@ -169,11 +140,23 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
if err != nil {
return nil, err
}
res, err := doLocalRequestNiceError(req)
res, err := DoLocalRequest(req)
if err != nil {
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
pathPrefix := path
if i := strings.Index(path, "?"); i != -1 {
pathPrefix = path[:i]
}
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
defer res.Body.Close()
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
@@ -245,7 +228,7 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "?peers=false")
}
@@ -312,30 +295,6 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
return fts, nil
}
// PushFile sends Taildrop file r to target.
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
}
if size != -1 {
req.ContentLength = size
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(io.Discard, res.Body)
return nil
}
all, _ := io.ReadAll(res.Body)
return fmt.Errorf("%s: %s", res.Status, all)
}
func CheckIPForwarding(ctx context.Context) error {
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {

View File

@@ -12,7 +12,6 @@ import (
"errors"
"expvar"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
@@ -37,7 +36,6 @@ import (
var (
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server address")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
@@ -252,26 +250,24 @@ func main() {
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
mux.ServeHTTP(w, r)
})
if *httpPort > -1 {
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
// and not hit net/http/pprof's "profile
// duration exceeds server's WriteTimeout".
WriteTimeout: 5 * time.Minute,
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, "80"),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
// and not hit net/http/pprof's "profile
// duration exceeds server's WriteTimeout".
WriteTimeout: 5 * time.Minute,
}
err := port80srv.ListenAndServe()
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
err := port80srv.ListenAndServe()
if err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
}
}()
}
}
}()
err = httpsrv.ListenAndServeTLS("", "")
} else {
log.Printf("derper: serving on %s", *addr)

View File

@@ -164,11 +164,6 @@ change in the future.
}
tailscale.TailscaledSocket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
tailscale.TailscaledSocketSetExplicitly = true
}
})
err := rootCmd.Run(context.Background())
if errors.Is(err, flag.ErrHelp) {
@@ -196,8 +191,7 @@ var rootArgs struct {
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
c, err := safesocket.Connect(s)
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
if err != nil {
if runtime.GOOS != "windows" && rootArgs.socket == "" {
fatalf("--socket cannot be empty")

View File

@@ -18,10 +18,8 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/version/distro"
)
// geese is a collection of gooses. It need not be complete.
@@ -59,7 +57,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
curExitNodeIP netaddr.IP
curUser string // os.Getenv("USER") on the client side
goos string // empty means "linux"
distro distro.Distro
want string
}{
@@ -316,7 +313,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
RouteAll: true,
// And assume this no-op accidental pre-1.8 value:
NoSNAT: true,
@@ -333,7 +329,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
},
goos: "openbsd",
goos: "windows",
want: "", // not an error
},
{
@@ -409,21 +405,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeAllowLANAccess: true,
ExitNodeID: "some_stable_id",
},
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
},
{
name: "ignore_login_server_synonym",
flags: []string{"--login-server=https://controlplane.tailscale.com"},
@@ -446,38 +427,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// migth've had old an install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: distro.Synology,
want: "",
},
{
// Same test case as "synology_permit_omit_accept_routes" above, but
// on non-Synology distro.
name: "not_synology_dont_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
goos: "linux",
distro: "", // not Synology
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -498,7 +447,6 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
goos: goos,
flagSet: flagSet,
curExitNodeIP: tt.curExitNodeIP,
distro: tt.distro,
}); err != nil {
got = err.Error()
}
@@ -547,7 +495,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
WantRunning: true,
CorpDNS: true,
AllowSingleHosts: true,
RouteAll: true,
NetfilterMode: preftype.NetfilterOn,
},
},
@@ -585,7 +532,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
args: upArgsT{
exitNodeIP: "foo",
},
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
},
{
name: "error_exit_node_allow_lan_without_exit_node",
@@ -859,133 +806,3 @@ func TestUpdatePrefs(t *testing.T) {
})
}
}
func TestExitNodeIPOfArg(t *testing.T) {
mustIP := netaddr.MustParseIP
tests := []struct {
name string
arg string
st *ipnstate.Status
want netaddr.IP
wantErr string
}{
{
name: "ip_while_stopped_okay",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Stopped",
},
want: mustIP("1.2.3.4"),
},
{
name: "ip_not_found",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Running",
},
wantErr: `no node found in netmap with IP 1.2.3.4`,
},
{
name: "ip_not_exit",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Running",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
},
},
},
wantErr: `node 1.2.3.4 is not advertising an exit node`,
},
{
name: "ip",
arg: "1.2.3.4",
st: &ipnstate.Status{
BackendState: "Running",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
ExitNodeOption: true,
},
},
},
want: mustIP("1.2.3.4"),
},
{
name: "no_match",
arg: "unknown",
st: &ipnstate.Status{MagicDNSSuffix: ".foo"},
wantErr: `invalid value "unknown" for --exit-node; must be IP or unique node name`,
},
{
name: "name",
arg: "skippy",
st: &ipnstate.Status{
MagicDNSSuffix: ".foo",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
DNSName: "skippy.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
ExitNodeOption: true,
},
},
},
want: mustIP("1.0.0.2"),
},
{
name: "name_not_exit",
arg: "skippy",
st: &ipnstate.Status{
MagicDNSSuffix: ".foo",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
DNSName: "skippy.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
},
},
},
wantErr: `node "skippy" is not advertising an exit node`,
},
{
name: "ambiguous",
arg: "skippy",
st: &ipnstate.Status{
MagicDNSSuffix: ".foo",
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
key.NewNode().Public(): {
DNSName: "skippy.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
ExitNodeOption: true,
},
key.NewNode().Public(): {
DNSName: "SKIPPY.foo.",
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
ExitNodeOption: true,
},
},
},
wantErr: `ambiguous exit node name "skippy"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := exitNodeIPOfArg(tt.arg, tt.st)
if err != nil {
if err.Error() == tt.wantErr {
return
}
if tt.wantErr == "" {
t.Fatal(err)
}
t.Fatalf("error = %#q; want %#q", err, tt.wantErr)
}
if tt.wantErr != "" {
t.Fatalf("got %v; want error %#q", got, tt.wantErr)
}
if got != tt.want {
t.Fatalf("got %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -25,23 +25,23 @@ func fixTailscaledConnectError(origErr error) error {
if err != nil {
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
}
var foundProc ps.Process
found := false
for _, proc := range procs {
base := filepath.Base(proc.Executable())
if base == "tailscaled" {
foundProc = proc
found = true
break
}
if runtime.GOOS == "darwin" && base == "IPNExtension" {
foundProc = proc
found = true
break
}
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
foundProc = proc
found = true
break
}
}
if foundProc == nil {
if !found {
switch runtime.GOOS {
case "windows":
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
@@ -52,5 +52,5 @@ func fixTailscaledConnectError(origErr error) error {
}
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
}
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
}

View File

@@ -11,9 +11,11 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
@@ -28,7 +30,6 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
@@ -95,7 +96,7 @@ func runCp(ctx context.Context, args []string) error {
return err
}
stableID, isOffline, err := getTargetStableID(ctx, ip)
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
if err != nil {
return fmt.Errorf("can't send to %s: %v", target, err)
}
@@ -153,21 +154,32 @@ func runCp(ctx context.Context, args []string) error {
}
}
if cpArgs.verbose {
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
}
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
if err != nil {
return err
}
req.ContentLength = contentLength
if cpArgs.verbose {
log.Printf("sent %q", name)
log.Printf("sending to %v ...", dstURL)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
continue
}
io.Copy(Stdout, res.Body)
res.Body.Close()
return errors.New(res.Status)
}
return nil
}
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return "", false, err
@@ -183,7 +195,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
continue
}
isOffline = n.Online != nil && !*n.Online
return n.StableID, isOffline, nil
return ft.PeerAPIURL, isOffline, nil
}
}
return "", false, fileTargetErrorDetail(ctx, ip)

View File

@@ -29,22 +29,7 @@ var statusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status [--active] [--web] [--json]",
ShortHelp: "Show state of tailscaled and its connections",
LongHelp: strings.TrimSpace(`
JSON FORMAT
Warning: this format has changed between releases and might change more
in the future.
For a description of the fields, see the "type Status" declaration at:
https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
(and be sure to select branch/tag that corresponds to the version
of Tailscale you're running)
`),
Exec: runStatus,
Exec: runStatus,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("status")
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
@@ -160,19 +145,11 @@ func runStatus(ctx context.Context, args []string) error {
)
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
var offline string
if !ps.Online {
offline = "; offline"
}
if !ps.Active {
if ps.ExitNode {
f("idle; exit node" + offline)
} else if ps.ExitNodeOption {
f("idle; offers exit node" + offline)
f("idle; exit node")
} else if anyTraffic {
f("idle" + offline)
} else if !ps.Online {
f("offline")
f("idle")
} else {
f("-")
}
@@ -180,17 +157,12 @@ func runStatus(ctx context.Context, args []string) error {
f("active; ")
if ps.ExitNode {
f("exit node; ")
} else if ps.ExitNodeOption {
f("offers exit node; ")
}
if relay != "" && ps.CurAddr == "" {
f("relay %q", relay)
} else if ps.CurAddr != "" {
f("direct %s", ps.CurAddr)
}
if !ps.Online {
f("; offline")
}
}
if anyTraffic {
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)

View File

@@ -6,8 +6,6 @@ package cli
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
@@ -30,8 +28,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
"tailscale.com/util/dnsname"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -50,10 +46,8 @@ down").
If flags are specified, the flags must be the complete set of desired
settings. An error is returned if any setting would be changed as a
result of an unspecified flag's default value, unless the --reset flag
is also used. (The flags --authkey, --force-reauth, and --qr are not
considered settings that need to be re-specified when modifying
settings.)
result of an unspecified flag's default value, unless the --reset
flag is also used.
`),
FlagSet: upFlagSet,
Exec: runUp,
@@ -66,34 +60,20 @@ func effectiveGOOS() string {
return runtime.GOOS
}
// acceptRouteDefault returns the CLI's default value of --accept-routes as
// a function of the platform it's running on.
func acceptRouteDefault(goos string) bool {
switch goos {
case "windows":
return true
case "darwin":
return version.IsSandboxedMacOS()
default:
return false
}
}
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf := newFlagSet("up")
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
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 exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
@@ -141,7 +121,6 @@ type upArgsT struct {
authKeyOrFile string // "secret" or "file:/path/to/secret"
hostname string
opUser string
json bool
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -159,33 +138,6 @@ func (a upArgsT) getAuthKey() (string, error) {
var upArgs upArgsT
// Fields output when `tailscale up --json` is used. Two JSON blocks will be output.
//
// When "tailscale up" is run it first outputs a block with AuthURL and QR populated,
// providing the link for where to authenticate this client. BackendState would be
// valid but boring, as it will almost certainly be "NeedsLogin". Error would be
// populated if something goes badly wrong.
//
// When the client is authenticated by having someone visit the AuthURL, a second
// JSON block will be output. The AuthURL and QR fields will not be present, the
// BackendState and Error fields will give the result of the authentication.
// Ex:
// {
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
// "QR": "data:image/png;base64,0123...cdef"
// "BackendState": "NeedsLogin"
// }
// {
// "BackendState": "Running"
// }
//
type upOutputJSON struct {
AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789
QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL
BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth
Error string `json:",omitempty"` // description of an error
}
func warnf(format string, args ...interface{}) {
printf("Warning: "+format+"\n", args...)
}
@@ -238,65 +190,6 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
return routes, nil
}
// peerWithTailscaleIP returns the peer in st with the provided
// Tailscale IP.
func peerWithTailscaleIP(st *ipnstate.Status, ip netaddr.IP) (ps *ipnstate.PeerStatus, ok bool) {
for _, ps := range st.Peer {
for _, ip2 := range ps.TailscaleIPs {
if ip == ip2 {
return ps, true
}
}
}
return nil, false
}
// exitNodeIPOfArg maps from a user-provided CLI flag value to an IP
// address they want to use as an exit node.
func exitNodeIPOfArg(arg string, st *ipnstate.Status) (ip netaddr.IP, err error) {
if arg == "" {
return ip, errors.New("invalid use of exitNodeIPOfArg with empty string")
}
ip, err = netaddr.ParseIP(arg)
if err == nil {
// If we're online already and have a netmap, double check that the IP
// address specified is valid.
if st.BackendState == "Running" {
ps, ok := peerWithTailscaleIP(st, ip)
if !ok {
return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
}
if !ps.ExitNodeOption {
return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
}
}
return ip, err
}
match := 0
for _, ps := range st.Peer {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if !strings.EqualFold(arg, baseName) {
continue
}
match++
if len(ps.TailscaleIPs) == 0 {
return ip, fmt.Errorf("node %q has no Tailscale IP?", arg)
}
if !ps.ExitNodeOption {
return ip, fmt.Errorf("node %q is not advertising an exit node", arg)
}
ip = ps.TailscaleIPs[0]
}
switch match {
case 0:
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", arg)
case 1:
return ip, nil
default:
return ip, fmt.Errorf("ambiguous exit node name %q", arg)
}
}
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
//
// Note that the parameters upArgs and warnf are named intentionally
@@ -312,9 +205,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
var exitNodeIP netaddr.IP
if upArgs.exitNodeIP != "" {
var err error
exitNodeIP, err = exitNodeIPOfArg(upArgs.exitNodeIP, st)
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
if err != nil {
return nil, err
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
}
} else if upArgs.exitNodeAllowLANAccess {
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
@@ -487,12 +380,11 @@ func runUp(ctx context.Context, args []string) error {
env := upCheckEnv{
goos: effectiveGOOS(),
distro: distro.Get(),
user: os.Getenv("USER"),
flagSet: upFlagSet,
upArgs: upArgs,
backendState: st.BackendState,
curExitNodeIP: exitNodeIP(curPrefs, st),
curExitNodeIP: exitNodeIP(prefs, st),
}
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
if err != nil {
@@ -543,16 +435,10 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive()
case ipn.NeedsMachineAuth:
printed = true
if env.upArgs.json {
printUpDoneJSON(ipn.NeedsMachineAuth, "")
} else {
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
}
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
case ipn.Running:
// Done full authentication process
if env.upArgs.json {
printUpDoneJSON(ipn.Running, "")
} else if printed {
if printed {
// Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(Stderr, "Success.\n")
}
@@ -565,33 +451,15 @@ func runUp(ctx context.Context, args []string) error {
}
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
printed = true
if upArgs.json {
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr {
q, err := qrcode.New(*url, qrcode.Medium)
if err == nil {
png, err := q.PNG(128)
if err == nil {
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
}
if err != nil {
log.Printf("QR code error: %v", err)
} else {
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
}
data, err := json.MarshalIndent(js, "", "\t")
if err != nil {
log.Printf("upOutputJSON marshalling error: %v", err)
} else {
fmt.Println(string(data))
}
} else {
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr {
q, err := qrcode.New(*url, qrcode.Medium)
if err != nil {
log.Printf("QR code error: %v", err)
} else {
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
}
}
}
}
})
@@ -678,16 +546,6 @@ func runUp(ctx context.Context, args []string) error {
}
}
func printUpDoneJSON(state ipn.State, errorString string) {
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
data, err := json.MarshalIndent(js, "", " ")
if err != nil {
log.Printf("printUpDoneJSON marshalling error: %v", err)
} else {
fmt.Println(string(data))
}
}
var (
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
)
@@ -730,7 +588,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
// correspond to an ipn.Pref.
func preflessFlag(flagName string) bool {
switch flagName {
case "authkey", "force-reauth", "reset", "qr", "json":
case "authkey", "force-reauth", "reset", "qr":
return true
}
return false
@@ -764,7 +622,6 @@ type upCheckEnv struct {
upArgs upArgsT
backendState string
curExitNodeIP netaddr.IP
distro distro.Distro
}
// checkForAccidentalSettingReverts (the "up checker") checks for
@@ -815,10 +672,6 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
continue
}
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
continue
}
missing = append(missing, fmtFlagValueArg(flagName, valCur))
}
if len(missing) == 0 {

View File

@@ -3,7 +3,6 @@ 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/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
L github.com/klauspost/compress/flate from nhooyr.io/websocket
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
@@ -73,7 +72,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
@@ -92,7 +91,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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/salsa20/salsa from golang.org/x/crypto/nacl/box+
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

View File

@@ -25,8 +25,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
@@ -63,7 +62,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/net/dns
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from inet.af/netstack/tcpip/header+
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
@@ -181,7 +179,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
tailscale.com/kube from tailscale.com/ipn
W tailscale.com/log/filelogger from tailscale.com/logpolicy
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
tailscale.com/logtail from tailscale.com/logpolicy+
@@ -195,17 +193,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/netknob from tailscale.com/logpolicy+
tailscale.com/net/netknob from tailscale.com/ipn/localapi+
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
tailscale.com/net/socks5/tssocks 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/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
💣 tailscale.com/paths from tailscale.com/client/tailscale+
@@ -253,7 +250,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/tailscaled+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
@@ -276,9 +273,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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 from golang.org/x/net/http2/h2c+
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
golang.org/x/net/http2/hpack 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 golang.zx2c4.com/wireguard/device
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
@@ -369,7 +364,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints+
regexp/syntax from regexp
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+

View File

@@ -28,16 +28,13 @@ import (
"syscall"
"time"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/net/socks5/tssocks"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
@@ -178,7 +175,8 @@ func main() {
osshare.SetFileSharingEnabled(false, logger.Discard)
if err != nil {
log.Fatal(err)
// No need to log; the func already did
os.Exit(1)
}
}
@@ -300,55 +298,43 @@ func run() error {
linkMon, err := monitor.New(logf)
if err != nil {
return fmt.Errorf("monitor.New: %w", err)
log.Fatalf("creating link monitor: %v", err)
}
pol.Logtail.SetLinkMonitor(linkMon)
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
dialer := new(tsdial.Dialer) // mutated below (before used)
e, useNetstack, err := createEngine(logf, linkMon, dialer)
e, useNetstack, err := createEngine(logf, linkMon)
if err != nil {
return fmt.Errorf("createEngine: %w", err)
logf("wgengine.New: %v", err)
return err
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
ns, err := newNetstack(logf, dialer, e)
ns, err := newNetstack(logf, e)
if err != nil {
return fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || wrapNetstack
if err := ns.Start(); err != nil {
return fmt.Errorf("failed to start netstack: %w", err)
}
if useNetstack {
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := e.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
log.Fatalf("failed to start netstack: %v", err)
}
if socksListener != nil || httpProxyListener != nil {
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
}
if socksListener != nil {
ss := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
Dialer: dialer.UserDial,
}
go func() {
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
}()
}
}
@@ -378,11 +364,13 @@ func run() error {
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
return fmt.Errorf("ipnserver.StateStore: %w", err)
logf("ipnserver.StateStore: %v", err)
return err
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
if err != nil {
return fmt.Errorf("ipnserver.New: %w", err)
logf("ipnserver.New: %v", err)
return err
}
if debugMux != nil {
@@ -397,20 +385,21 @@ func run() error {
err = srv.Run(ctx, ln)
// Cancelation is not an error: it is the only way to stop ipnserver.
if err != nil && err != context.Canceled {
return fmt.Errorf("ipnserver.Run: %w", err)
logf("ipnserver.Run: %v", err)
return err
}
return nil
}
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, useNetstack bool, err error) {
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
if args.tunname == "" {
return nil, false, errors.New("no --tun value specified")
}
var errs []error
for _, name := range strings.Split(args.tunname, ",") {
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
e, useNetstack, err = tryEngine(logf, linkMon, name)
if err == nil {
return e, useNetstack, nil
}
@@ -442,11 +431,10 @@ func shouldWrapNetstack() bool {
return false
}
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
LinkMonitor: linkMon,
Dialer: dialer,
}
useNetstack = name == "userspace-networking"
@@ -520,54 +508,26 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
}
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
return netstack.Create(logf, tunDev, e, magicConn, dialer)
return netstack.Create(logf, tunDev, e, magicConn)
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
// proxies, if the respective addresses are not empty. socksAddr and
// httpAddr can be the same, in which case socksListener will receive
// connections that look like they're speaking SOCKS and httpListener
// will receive everything else.
//
// socksListener and httpListener can be nil, if their respective
// addrs are empty.
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
ln, err := net.Listen("tcp", socksAddr)
if err != nil {
log.Fatalf("proxy listener: %v", err)
}
return proxymux.SplitSOCKSAndHTTP(ln)
func mustStartTCPListener(name, addr string) net.Listener {
if addr == "" {
return nil
}
var err error
if socksAddr != "" {
socksListener, err = net.Listen("tcp", socksAddr)
if err != nil {
log.Fatalf("SOCKS5 listener: %v", err)
}
if strings.HasSuffix(socksAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
}
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("%v listener: %v", name, err)
}
if httpAddr != "" {
httpListener, err = net.Listen("tcp", httpAddr)
if err != nil {
log.Fatalf("HTTP proxy listener: %v", err)
}
if strings.HasSuffix(httpAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
}
if strings.HasSuffix(addr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("%v listening on %v", name, ln.Addr())
}
return socksListener, httpListener
return ln
}

View File

@@ -32,7 +32,6 @@ import (
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/safesocket"
"tailscale.com/types/logger"
@@ -40,7 +39,6 @@ import (
"tailscale.com/version"
"tailscale.com/wf"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -80,10 +78,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
// Make a logger without a date prefix, as filelogger
// and logtail both already add their own. All we really want
// from the log package is the automatic newline.
// We start with log.Default().Writer(), which is the logtail
// writer that logpolicy already installed as the global
// output.
logger := log.New(log.Default().Writer(), "", 0)
logger := log.New(os.Stderr, "", 0)
ipnserver.BabysitProc(ctx, args, logger.Printf)
}()
@@ -119,9 +114,6 @@ func beWindowsSubprocess() bool {
}
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file logggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
@@ -185,12 +177,6 @@ func beFirewallKillswitch() bool {
func startIPNServer(ctx context.Context, logid string) error {
var logf logger.Logf = log.Printf
linkMon, err := monitor.New(logf)
if err != nil {
return err
}
dialer := new(tsdial.Dialer)
getEngineRaw := func() (wgengine.Engine, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
@@ -211,19 +197,17 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, fmt.Errorf("DNS: %w", err)
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
LinkMonitor: linkMon,
Dialer: dialer,
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
})
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("engine: %w", err)
}
ns, err := newNetstack(logf, dialer, eng)
ns, err := newNetstack(logf, eng)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
@@ -303,7 +287,7 @@ func startIPNServer(ctx context.Context, logid string) error {
return fmt.Errorf("safesocket.Listen: %v", err)
}
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
if err != nil {
logf("ipnserver.Run: %v", err)
}

View File

@@ -339,9 +339,11 @@ func (c *Auto) authRoutine() {
continue
}
if url != "" {
// goal.url ought to be empty here.
// However, not all control servers get this right,
// and logging about it here just generates noise.
if goal.url != "" {
err = fmt.Errorf("[unexpected] server required a new URL?")
report(err, "WaitLoginURL")
}
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: true,

View File

@@ -139,9 +139,6 @@ func TestNoReuse(t *testing.T) {
t.Fatalf("server wire traffic seen twice")
}
packets[serverWire] = true
server.Close()
client.Close()
}
}

245
go.mod
View File

@@ -7,257 +7,208 @@ require (
github.com/akutz/memconn v0.1.0
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2 v1.11.2
github.com/aws/aws-sdk-go-v2/config v1.11.0
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1
github.com/aws/aws-sdk-go v1.38.52
github.com/aws/aws-sdk-go-v2 v1.9.2
github.com/aws/aws-sdk-go-v2/config v1.8.3
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0
github.com/coreos/go-iptables v0.6.0
github.com/creack/pty v1.1.17
github.com/dave/jennifer v1.4.1
github.com/frankban/quicktest v1.14.0
github.com/gliderlabs/ssh v0.3.3
github.com/go-ole/go-ole v1.2.6
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1
github.com/godbus/dbus/v5 v5.0.6
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.3.0
github.com/goreleaser/nfpm v1.10.3
github.com/iancoleman/strcase v0.2.0
github.com/insomniacslk/dhcp v0.0.0-20211026125128-ad197bcd36fd
github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.13.6
github.com/mdlayher/netlink v1.4.2
github.com/mdlayher/netlink v1.4.1
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
github.com/miekg/dns v1.1.43
github.com/mitchellh/go-ps v1.0.0
github.com/pborman/getopt v1.1.0
github.com/peterbourgon/ff/v3 v3.1.2
github.com/peterbourgon/ff/v3 v3.1.0
github.com/pkg/sftp v1.13.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6
go4.org/mem v0.0.0-20210711025021-927187094b94
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/net v0.0.0-20211205041911-012df41ee64c
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
golang.org/x/net v0.0.0-20211111083644-e5c967477495
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.8
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
golang.org/x/tools v0.1.7
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45
golang.zx2c4.com/wireguard/windows v0.4.10
honnef.co/go/tools v0.2.2
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
nhooyr.io/websocket v1.8.7
)
require (
4d63.com/gochecknoglobals v0.1.0 // indirect
github.com/Antonboom/errname v0.1.5 // indirect
github.com/Antonboom/nilnil v0.1.0 // indirect
github.com/BurntSushi/toml v0.4.1 // indirect
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Djarvur/go-err113 v0.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/ashanbrown/forbidigo v1.2.0 // indirect
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 // indirect
github.com/aws/smithy-go v1.8.0 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/blizzy78/varnamelen v0.5.0 // indirect
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
github.com/breml/bidichk v0.2.1 // indirect
github.com/butuzov/ireturn v0.1.1 // indirect
github.com/bombsimon/wsl/v3 v3.1.0 // indirect
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.9 // indirect
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
github.com/daixiang0/gci v0.2.9 // indirect
github.com/daixiang0/gci v0.2.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingajkin/go-header v0.4.2 // indirect
github.com/denis-tingajkin/go-header v0.3.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/esimonov/ifshort v1.0.3 // indirect
github.com/ettle/strcase v0.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fzipp/gocyclo v0.3.1 // indirect
github.com/go-critic/go-critic v0.6.1 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-critic/go-critic v0.5.2 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-git/go-billy/v5 v5.0.0 // indirect
github.com/go-git/go-git/v5 v5.2.0 // indirect
github.com/go-toolsmith/astcast v1.0.0 // indirect
github.com/go-toolsmith/astcopy v1.0.0 // indirect
github.com/go-toolsmith/astequal v1.0.1 // indirect
github.com/go-toolsmith/astequal v1.0.0 // indirect
github.com/go-toolsmith/astfmt v1.0.0 // indirect
github.com/go-toolsmith/astp v1.0.0 // indirect
github.com/go-toolsmith/strparse v1.0.0 // indirect
github.com/go-toolsmith/typep v1.0.2 // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gofrs/flock v0.8.0 // indirect
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect
github.com/golangci/golangci-lint v1.43.0 // indirect
github.com/golangci/golangci-lint v1.33.0 // indirect
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc // indirect
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
github.com/golangci/misspell v0.3.5 // indirect
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 // indirect
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
github.com/goreleaser/chglog v0.1.2 // indirect
github.com/goreleaser/fileglob v0.3.1 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/gostaticanalysis/analysisutil v0.6.1 // indirect
github.com/gostaticanalysis/comment v1.4.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jgautheron/goconst v1.5.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 // indirect
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/kisielk/errcheck v1.6.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kulti/thelper v0.4.0 // indirect
github.com/kunwardeep/paralleltest v1.0.3 // indirect
github.com/kunwardeep/paralleltest v1.0.2 // indirect
github.com/kyoh86/exportloopref v0.1.8 // indirect
github.com/ldez/gomoddirectives v0.2.2 // indirect
github.com/ldez/tagliatelle v0.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/magiconair/properties v1.8.4 // indirect
github.com/maratori/testpackage v1.0.1 // indirect
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb // indirect
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
github.com/mgechev/revive v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/matoous/godox v0.0.0-20200801072554-4fb83dc2941e // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mbilski/exhaustivestruct v1.1.0 // indirect
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mitchellh/mapstructure v1.4.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/moricho/tparallel v0.2.1 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
github.com/nishanths/exhaustive v0.7.11 // indirect
github.com/nishanths/predeclared v0.2.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/nakabonne/nestif v0.3.0 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
github.com/nishanths/exhaustive v0.1.0 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.3.13 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/ryancurrah/gomodguard v1.2.3 // indirect
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f // indirect
github.com/quasilyte/go-ruleguard v0.2.1 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c // indirect
github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/ryancurrah/gomodguard v1.1.0 // indirect
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
github.com/securego/gosec/v2 v2.9.3 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/securego/gosec/v2 v2.5.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sivchari/tenv v1.4.7 // indirect
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/sonatard/noctx v0.0.1 // indirect
github.com/sourcegraph/go-diff v0.6.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.1.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.9.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/spf13/viper v1.7.1 // indirect
github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/sylvia7788/contextcheck v1.0.4 // indirect
github.com/tdakkota/asciicheck v0.1.1 // indirect
github.com/tetafro/godot v1.4.11 // indirect
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
github.com/tomarrell/wrapcheck/v2 v2.4.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b // indirect
github.com/tetafro/godot v1.3.2 // indirect
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 // indirect
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 // indirect
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa // indirect
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.0.4 // indirect
github.com/uudashr/gocognit v1.0.5 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/yeya24/promlinter v0.1.0 // indirect
github.com/uudashr/gocognit v1.0.1 // indirect
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
howett.net/plist v1.0.0 // indirect
mvdan.cc/gofumpt v0.2.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 // indirect
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 // indirect
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 // indirect
)

1088
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -232,11 +232,32 @@ func TestDNSConfigForNetmap(t *testing.T) {
},
},
{
// Prior to fixing https://github.com/tailscale/tailscale/issues/2116,
// Android had cases where it needed FallbackResolvers. This was the
// negative test for the case where Override-local-DNS was set, so the
// fallback resolvers did not need to be used. This test is still valid
// so we keep it, but the fallback test has been removed.
name: "android_does_need_fallbacks",
os: "android",
nm: &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
FallbackResolvers: []dnstype.Resolver{
{Addr: "8.8.4.4"},
},
Routes: map[string][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4"}},
},
},
},
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netaddr.IP{},
DefaultResolvers: []dnstype.Resolver{
{Addr: "8.8.4.4:53"},
},
Routes: map[dnsname.FQDN][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4:53"}},
},
},
},
{
name: "android_does_NOT_need_fallbacks",
os: "android",
nm: &netmap.NetworkMap{
@@ -323,48 +344,3 @@ func TestDNSConfigForNetmap(t *testing.T) {
})
}
}
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
b := &LocalBackend{}
if b.allowExitNodeDNSProxyToServeName("google.com") {
t.Fatal("unexpected true on backend with nil NetMap")
}
b.netMap = &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
ExitNodeFilteredSet: []string{
".ts.net",
"some.exact.bad",
},
},
}
tests := []struct {
name string
want bool
}{
// Allow by default:
{"google.com", true},
{"GOOGLE.com", true},
// Rejected by suffix:
{"foo.TS.NET", false},
{"foo.ts.net", false},
// Suffix doesn't match
{"ts.net", true},
// Rejected by exact match:
{"some.exact.bad", false},
{"SOME.EXACT.BAD", false},
// But a prefix is okay.
{"prefix-okay.some.exact.bad", true},
}
for _, tt := range tests {
got := b.allowExitNodeDNSProxyToServeName(tt.name)
if got != tt.want {
t.Errorf("for %q = %v; want %v", tt.name, got, tt.want)
}
}
}

View File

@@ -22,6 +22,7 @@ import (
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"inet.af/netaddr"
@@ -35,7 +36,6 @@ import (
"tailscale.com/net/dns"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
@@ -88,7 +88,6 @@ type LocalBackend struct {
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store ipn.StateStore
dialer *tsdial.Dialer // non-nil
backendLogID string
unregisterLinkMon func()
unregisterHealthWatch func()
@@ -142,11 +141,7 @@ type LocalBackend struct {
// same as the Network Extension lifetime and we can thus avoid
// double-copying files by writing them to the right location
// immediately.
// It's also used on Synology & TrueNAS, but in that case DoFinalRename
// is also set true, which moves the *.partial file to its final
// name on completion.
directFileRoot string
directFileDoFinalRename bool // false on macOS, true on Synology & TrueNAS
directFileRoot string
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -160,14 +155,9 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
// NewLocalBackend returns a new LocalBackend that is ready to run,
// but is not actually running.
//
// If dialer is nil, a new one is made.
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine) (*LocalBackend, error) {
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
if e == nil {
panic("ipn.NewLocalBackend: engine must not be nil")
}
if dialer == nil {
dialer = new(tsdial.Dialer)
panic("ipn.NewLocalBackend: wgengine must not be nil")
}
osshare.SetFileSharingEnabled(false, logf)
@@ -186,13 +176,11 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
store: store,
dialer: dialer,
backendLogID: logid,
state: ipn.NoState,
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
}
// Default filter blocks everything and logs nothing, until Start() is called.
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
@@ -222,11 +210,6 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
return b, nil
}
// Dialer returns the backend's dialer.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
}
// SetDirectFileRoot sets the directory to download files to directly,
// without buffering them through an intermediate daemon-owned
// tailcfg.UserID-specific directory.
@@ -238,17 +221,6 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
b.directFileRoot = dir
}
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
// a received "name.partial" file to "name" when the download is complete.
//
// This only applies when SetDirectFileRoot is non-empty.
// The default is false.
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.directFileDoFinalRename = v
}
// b.mu must be held.
func (b *LocalBackend) maybePauseControlClientLocked() {
if b.cc == nil {
@@ -417,30 +389,33 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
if p.LastSeen != nil {
lastSeen = *p.LastSeen
}
var tailAddr4 string
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
for _, addr := range p.Addresses {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
if addr.IP().Is4() && tailAddr4 == "" {
// The peer struct previously only allowed a single
// Tailscale IP address. For compatibility for a few releases starting
// with 1.8, keep it pulled out as IPv4-only for a bit.
tailAddr4 = addr.IP().String()
}
tailscaleIPs = append(tailscaleIPs, addr.IP())
}
}
exitNodeOption := tsaddr.PrefixesContainsFunc(p.AllowedIPs, func(r netaddr.IPPrefix) bool {
return r.Bits() == 0
})
sb.AddPeer(p.Key, &ipnstate.PeerStatus{
InNetworkMap: true,
ID: p.StableID,
UserID: p.User,
TailscaleIPs: tailscaleIPs,
HostName: p.Hostinfo.Hostname,
DNSName: p.Name,
OS: p.Hostinfo.OS,
KeepAlive: p.KeepAlive,
Created: p.Created,
LastSeen: lastSeen,
Online: p.Online != nil && *p.Online,
ShareeNode: p.Hostinfo.ShareeNode,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
ExitNodeOption: exitNodeOption,
InNetworkMap: true,
ID: p.StableID,
UserID: p.User,
TailAddrDeprecated: tailAddr4,
TailscaleIPs: tailscaleIPs,
HostName: p.Hostinfo.Hostname,
DNSName: p.Name,
OS: p.Hostinfo.OS,
KeepAlive: p.KeepAlive,
Created: p.Created,
LastSeen: lastSeen,
ShareeNode: p.Hostinfo.ShareeNode,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
})
}
}
@@ -625,11 +600,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// 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 nm == nil {
// No netmap, can't resolve anything.
return false
}
// If we have a desired IP on file, try to find the corresponding
// node.
if b.prefs.ExitNodeIP.IsZero() {
@@ -1289,7 +1259,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
return
}
if apiSrv.hasFilesWaiting() {
if apiSrv != nil && apiSrv.hasFilesWaiting() {
n.FilesWaiting = &empty.Message{}
}
@@ -1698,7 +1668,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
}
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
// unlocks b.mu when done. newp ownership passes to this function.
// unlocks b.mu when done.
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
netMap := b.netMap
stateKey := b.stateKey
@@ -1706,10 +1676,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
oldp := b.prefs
newp.Persist = oldp.Persist // caller isn't allowed to override this
b.prefs = newp
// findExitNodeIDLocked returns whether it updated b.prefs, but
// everything in this function treats b.prefs as completely new
// anyway. No-op if no exit node resolution is needed.
b.findExitNodeIDLocked(netMap)
b.inServerMode = newp.ForceDaemon
// We do this to avoid holding the lock while doing everything else.
newp = b.prefs.Clone()
@@ -1787,24 +1753,15 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
for _, pln := range b.peerAPIListeners {
proto := tailcfg.PeerAPI4
proto := tailcfg.ServiceProto("peerapi4")
if pln.ip.Is6() {
proto = tailcfg.PeerAPI6
proto = "peerapi6"
}
ret = append(ret, tailcfg.Service{
Proto: proto,
Port: uint16(pln.port),
})
}
switch runtime.GOOS {
case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows":
// These are the platforms currently supported by
// net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery.
ret = append(ret, tailcfg.Service{
Proto: tailcfg.PeerAPIDNS,
Port: 1, // version
})
}
return ret
}
@@ -1905,15 +1862,6 @@ func (b *LocalBackend) authReconfig() {
}
}
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
}
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID)
if err != nil {
b.logf("wgcfg: %v", err)
@@ -2012,32 +1960,12 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
return dcfg
}
for _, dom := range nm.DNS.Domains {
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
logf("[unexpected] non-FQDN search domain %q", dom)
}
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
}
if nm.DNS.Proxied { // actually means "enable MagicDNS"
for _, dom := range magicDNSRootDomains(nm) {
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
}
}
addDefault := func(resolvers []dnstype.Resolver) {
for _, r := range resolvers {
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
}
}
// If we're using an exit node and that exit node is new enough (1.19.x+)
// to run a DoH DNS proxy, then send all our DNS traffic through it.
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
addDefault([]dnstype.Resolver{{Addr: dohURL}})
return dcfg
}
addDefault(nm.DNS.Resolvers)
for suffix, resolvers := range nm.DNS.Routes {
fqdn, err := dnsname.ToFQDN(suffix)
@@ -2059,6 +1987,18 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
}
}
for _, dom := range nm.DNS.Domains {
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
logf("[unexpected] non-FQDN search domain %q", dom)
}
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
}
if nm.DNS.Proxied { // actually means "enable MagicDNS"
for _, dom := range magicDNSRootDomains(nm) {
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
}
}
// Set FallbackResolvers as the default resolvers in the
// scenarios that can't handle a purely split-DNS config. See
@@ -2082,6 +2022,9 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
addDefault(nm.DNS.FallbackResolvers)
case len(dcfg.Routes) == 0:
// No settings requiring split DNS, no problem.
case versionOS == "android":
// We don't support split DNS at all on Android yet.
addDefault(nm.DNS.FallbackResolvers)
}
return dcfg
@@ -2128,7 +2071,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
}
varRoot := b.TailscaleVarRoot()
if varRoot == "" {
b.logf("Taildrop disabled; no state directory")
b.logf("peerapi disabled; no state directory")
return ""
}
baseDir := fmt.Sprintf("%s-uid-%d",
@@ -2136,7 +2079,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
uid)
dir := filepath.Join(varRoot, "files", baseDir)
if err := os.MkdirAll(dir, 0700); err != nil {
b.logf("Taildrop disabled; error making directory: %v", err)
b.logf("peerapi disabled; error making directory: %v", err)
return ""
}
return dir
@@ -2199,15 +2142,22 @@ func (b *LocalBackend) initPeerAPIListener() {
fileRoot := b.fileRootLocked(selfNode.User)
if fileRoot == "" {
b.logf("peerapi starting without Taildrop directory configured")
return
}
var tunName string
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
if tunWrap, _, ok := ge.GetInternals(); ok {
tunName, _ = tunWrap.Name()
}
}
ps := &peerAPIServer{
b: b,
rootDir: fileRoot,
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
directFileDoFinalRename: b.directFileDoFinalRename,
b: b,
rootDir: fileRoot,
tunName: tunName,
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
}
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
if r, ok := re.GetResolver(); ok {
@@ -2690,7 +2640,6 @@ func hasCapability(nm *netmap.NetworkMap, cap string) bool {
}
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.dialer.SetNetMap(nm)
var login string
if nm != nil {
login = nm.UserProfiles[nm.User].LoginName
@@ -2795,6 +2744,9 @@ func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
if apiSrv == nil {
return nil, errors.New("peerapi disabled")
}
return apiSrv.WaitingFiles()
}
@@ -2802,6 +2754,9 @@ func (b *LocalBackend) DeleteFile(name string) error {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
if apiSrv == nil {
return errors.New("peerapi disabled")
}
return apiSrv.DeleteFile(name)
}
@@ -2809,6 +2764,9 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
if apiSrv == nil {
return nil, 0, errors.New("peerapi disabled")
}
return apiSrv.OpenFile(name)
}
@@ -2922,9 +2880,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
var p4, p6 uint16
for _, s := range peer.Hostinfo.Services {
switch s.Proto {
case tailcfg.PeerAPI4:
case "peerapi4":
p4 = s.Port
case tailcfg.PeerAPI6:
case "peerapi6":
p6 = s.Port
}
}
@@ -3051,6 +3009,19 @@ func disabledSysctls(sysctls ...string) (disabled []string, err error) {
return disabled, nil
}
// peerDialControlFunc is non-nil on platforms that require a way to
// bind to dial out to other peers.
var peerDialControlFunc func(*LocalBackend) func(network, address string, c syscall.RawConn) error
// PeerDialControlFunc returns a net.Dialer.Control func (possibly nil) to use to
// dial other Tailscale peers from the current environment.
func (b *LocalBackend) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
if peerDialControlFunc != nil {
return peerDialControlFunc(b)
}
return nil
}
// DERPMap returns the current DERPMap in use, or nil if not connected.
func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
b.mu.Lock()
@@ -3082,55 +3053,3 @@ func (b *LocalBackend) OfferingExitNode() bool {
}
return def4 && def6
}
// allowExitNodeDNSProxyToServeName reports whether the Exit Node DNS
// proxy is allowed to serve responses for the provided DNS name.
func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool {
b.mu.Lock()
defer b.mu.Unlock()
nm := b.netMap
if nm == nil {
return false
}
name = strings.ToLower(name)
for _, bad := range nm.DNS.ExitNodeFilteredSet {
if bad == "" {
// Invalid, ignore.
continue
}
if bad[0] == '.' {
// Entries beginning with a dot are suffix matches.
if dnsname.HasSuffix(name, bad) {
return false
}
continue
}
// Otherwise entries are exact matches. They're
// guaranteed to be lowercase already.
if name == bad {
return false
}
}
return true
}
// exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters
// to exitNodeID's DoH service, if available.
//
// If exitNodeID is the zero valid, it returns "", false.
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
if exitNodeID.IsZero() {
return "", false
}
for _, p := range nm.Peers {
if p.StableID != exitNodeID {
continue
}
for _, s := range p.Hostinfo.Services {
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
return peerAPIBase(nm, p) + "/dns-query", true
}
}
}
return "", false
}

View File

@@ -92,14 +92,14 @@ func TestNetworkMapCompare(t *testing.T) {
},
{
"Node names identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
&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{{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
false,
},
{
@@ -117,8 +117,8 @@ func TestNetworkMapCompare(t *testing.T) {
{
"Node Users differ",
// User field is not checked.
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
true,
},
}
@@ -445,7 +445,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(eng.Close)
lb, err := NewLocalBackend(logf, "logid", store, nil, eng)
lb, err := NewLocalBackend(logf, "logid", store, eng)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}

View File

@@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
}
t.Cleanup(e.Close)
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e)
lb, err := NewLocalBackend(logf, idA.String(), store, e)
if err != nil {
t.Fatal(err)
}

View File

@@ -46,13 +46,10 @@ import (
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
// addH2C is non-nil on platforms where we want to add H2C
// ("cleartext" HTTP/2) support to the peerAPI.
var addH2C func(*http.Server)
type peerAPIServer struct {
b *LocalBackend
rootDir string // empty means file receiving unavailable
rootDir string
tunName string
selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool
resolver *resolver.Resolver
@@ -60,17 +57,10 @@ type peerAPIServer struct {
// directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on the GUI macOS versions
// and on Synology.
// somewhere itself. This is used on GUI macOS version.
// In directFileMode, the peerapi doesn't do the final rename
// from "foo.jpg.partial" to "foo.jpg" unless
// directFileDoFinalRename is set.
// from "foo.jpg.partial" to "foo.jpg".
directFileMode bool
// directFileDoFinalRename is whether in directFileMode we
// additionally move the *.direct file to its final name after
// it's received.
directFileDoFinalRename bool
}
const (
@@ -87,10 +77,6 @@ const (
deletedSuffix = ".deleted"
)
func (s *peerAPIServer) canReceiveFiles() bool {
return s != nil && s.rootDir != ""
}
func validFilenameRune(r rune) bool {
switch r {
case '/':
@@ -137,7 +123,7 @@ func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
// hasFilesWaiting reports whether any files are buffered in the
// tailscaled daemon storage.
func (s *peerAPIServer) hasFilesWaiting() bool {
if s == nil || s.rootDir == "" || s.directFileMode {
if s.rootDir == "" || s.directFileMode {
return false
}
if s.knownEmpty.Get() {
@@ -197,11 +183,8 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
// As a side effect, it also does any lazy deletion of files as
// required by Windows.
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s == nil {
return nil, errNilPeerAPIServer
}
if s.rootDir == "" {
return nil, errNoTaildrop
return nil, errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return nil, nil
@@ -265,11 +248,6 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
return ret, nil
}
var (
errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
)
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
// it failed earlier. This happens on Windows when various anti-virus
// tools hook into filesystem operations and have the file open still
@@ -285,11 +263,8 @@ func tryDeleteAgain(fullPath string) {
}
func (s *peerAPIServer) DeleteFile(baseName string) error {
if s == nil {
return errNilPeerAPIServer
}
if s.rootDir == "" {
return errNoTaildrop
return errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return errors.New("deletes not allowed in direct mode")
@@ -354,11 +329,8 @@ func touchFile(path string) error {
}
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if s == nil {
return nil, 0, errNilPeerAPIServer
}
if s.rootDir == "" {
return nil, 0, errNoTaildrop
return nil, 0, errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return nil, 0, errors.New("opens not allowed in direct mode")
@@ -391,7 +363,7 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
// On iOS/macOS, this sets the lc.Control hook to
// setsockopt the interface index to bind to, to get
// out of the network sandbox.
if err := initListenConfig(&lc, ip, ifState, s.b.dialer.TUNName()); err != nil {
if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
return nil, err
}
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
@@ -496,9 +468,6 @@ func (pln *peerAPIListener) serve() {
httpServer := &http.Server{
Handler: h,
}
if addH2C != nil {
addH2C(httpServer)
}
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
}
}
@@ -639,7 +608,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
return
}
if h.ps.rootDir == "" {
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
http.Error(w, "no rootdir", http.StatusInternalServerError)
return
}
rawPath := r.URL.EscapedPath()
@@ -711,7 +680,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
if h.ps.directFileMode {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
@@ -863,7 +832,7 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
defer cancel()
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr, h.ps.b.allowExitNodeDNSProxyToServeName)
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr)
if err != nil {
h.logf("handleDNS fwd error: %v", err)
if err := ctx.Err(); err != nil {
@@ -880,7 +849,7 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
return
}
w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Content-Length", strconv.Itoa(len(res)))
w.Header().Set("Content-Length", strconv.Itoa(len(q)))
w.Write(res)
}
@@ -949,19 +918,14 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
j, _ := json.Marshal(struct {
Error string
}{err.Error()})
j = append(j, '\n')
w.Write(j)
return
}
}()
var p dnsmessage.Parser
hdr, err := p.Start(res)
if err != nil {
if _, err := p.Start(res); err != nil {
return err
}
if hdr.RCode != dnsmessage.RCodeSuccess {
return fmt.Errorf("DNS RCode = %v", hdr.RCode)
}
if err := p.SkipAllQuestions(); err != nil {
return err
}

View File

@@ -1,22 +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.
//go:build !ios && !android
// +build !ios,!android
package ipnlocal
import (
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func init() {
addH2C = func(s *http.Server) {
h2s := &http2.Server{}
s.Handler = h2c.NewHandler(s.Handler, h2s)
}
}

View File

@@ -9,8 +9,10 @@
package ipnlocal
import (
"errors"
"fmt"
"net"
"syscall"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
@@ -19,6 +21,7 @@ import (
func init() {
initListenConfig = initListenConfigNetworkExtension
peerDialControlFunc = peerDialControlFuncNetworkExtension
}
// initListenConfigNetworkExtension configures nc for listening on IP
@@ -31,3 +34,24 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
}
return netns.SetListenConfigInterfaceIndex(nc, tunIf.Index)
}
func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error {
b.mu.Lock()
defer b.mu.Unlock()
st := b.prevIfState
pas := b.peerAPIServer
index := -1
if st != nil && pas != nil && pas.tunName != "" {
if tunIf, ok := st.Interface[pas.tunName]; ok {
index = tunIf.Index
}
}
var lc net.ListenConfig
netns.SetListenConfigInterfaceIndex(&lc, index)
return func(network, address string, c syscall.RawConn) error {
if index == -1 {
return errors.New("failed to find TUN interface to bind to")
}
return lc.Control(network, address, c)
}
}

View File

@@ -179,7 +179,7 @@ func TestHandlePeerAPI(t *testing.T) {
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(http.StatusInternalServerError),
bodyContains("Taildrop disabled; no storage directory"),
bodyContains("no rootdir"),
),
},
{

View File

@@ -284,7 +284,7 @@ func TestStateMachine(t *testing.T) {
t.Cleanup(e.Close)
cc := newMockControl(t)
b, err := NewLocalBackend(logf, "logid", store, nil, e)
b, err := NewLocalBackend(logf, "logid", store, e)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
@@ -941,7 +941,7 @@ func TestWGEngineStatusRace(t *testing.T) {
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
c.Assert(err, qt.IsNil)
t.Cleanup(eng.Close)
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng)
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), eng)
c.Assert(err, qt.IsNil)
cc := newMockControl(t)

View File

@@ -35,9 +35,9 @@ import (
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/ipn/store/aws"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
@@ -48,7 +48,6 @@ import (
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
)
// Options is the configuration of the Tailscale node agent.
@@ -652,7 +651,7 @@ func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) {
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
//
// Deprecated: use New and Server.Run instead.
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
getEngine = getEngineUntilItWorksWrapper(getEngine)
runDone := make(chan struct{})
defer close(runDone)
@@ -736,7 +735,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
}
}
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
server, err := New(logf, logid, store, eng, serverModeUser, opts)
if err != nil {
return err
}
@@ -749,8 +748,8 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
// New returns a new Server.
//
// To start it, use the Server.Run method.
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, dialer *tsdial.Dialer, serverModeUser *user.User, opts Options) (*Server, error) {
b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, serverModeUser *user.User, opts Options) (*Server, error) {
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
if err != nil {
return nil, fmt.Errorf("NewLocalBackend: %v", err)
}
@@ -759,22 +758,6 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
return smallzstd.NewDecoder(nil)
})
dg := distro.Get()
switch dg {
case distro.Synology, distro.TrueNAS:
// See if they have a "Taildrop" share.
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
path, err := findTaildropDir(dg)
if err != nil {
logf("%s Taildrop support: %v", dg, err)
} else {
logf("%s Taildrop: using %v", dg, path)
b.SetDirectFileRoot(path)
b.SetDirectFileDoFinalRename(true)
}
}
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
@@ -868,6 +851,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
panic("cannot determine executable: " + err.Error())
}
if runtime.GOOS == "windows" {
if len(args) != 2 && args[0] != "/subproc" {
panic(fmt.Sprintf("unexpected arguments %q", args))
}
logID := args[1]
logf = filelogger.New("tailscale-service", logID, logf)
}
var proc struct {
mu sync.Mutex
p *os.Process
@@ -1121,50 +1112,3 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
}
return ln.Listener.Accept()
}
func findTaildropDir(dg distro.Distro) (string, error) {
const name = "Taildrop"
switch dg {
case distro.Synology:
return findSynologyTaildropDir(name)
case distro.TrueNAS:
return findTrueNASTaildropDir(name)
}
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
}
// findSynologyTaildropDir looks for the first volume containing a
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
// but on DSM7 at least, we lack permissions to run that.
func findSynologyTaildropDir(name string) (dir string, err error) {
for i := 1; i <= 16; i++ {
dir = fmt.Sprintf("/volume%v/%s", i, name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findTrueNASTaildropDir returns the first matching directory of
// /mnt/{name} or /mnt/*/{name}
func findTrueNASTaildropDir(name string) (dir string, err error) {
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
dir = fmt.Sprintf("/mnt/%s", name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := ioutil.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
for _, fi := range fis {
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}

View File

@@ -13,7 +13,6 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnserver"
"tailscale.com/net/tsdial"
"tailscale.com/safesocket"
"tailscale.com/wgengine"
)
@@ -33,11 +32,10 @@ func TestRunMultipleAccepts(t *testing.T) {
t.Logf(format, args...)
}
s := safesocket.DefaultConnectionStrategy(socketPath)
connect := func() {
for i := 1; i <= 2; i++ {
logf("connect %d ...", i)
c, err := safesocket.Connect(s)
c, err := safesocket.Connect(socketPath, 0)
if err != nil {
t.Fatalf("safesocket.Connect: %v\n", err)
}
@@ -74,6 +72,6 @@ func TestRunMultipleAccepts(t *testing.T) {
}
defer ln.Close()
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", ipnserver.FixedEngine(eng), opts)
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, "dummy_logid", ipnserver.FixedEngine(eng), opts)
t.Logf("ipnserver.Run = %v", err)
}

View File

@@ -88,23 +88,22 @@ type PeerStatus struct {
OS string // HostInfo.OS
UserID tailcfg.UserID
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
// Endpoints:
Addrs []string
CurAddr string // one of Addrs, or unique if roaming
Relay string // DERP region
RxBytes int64
TxBytes int64
Created time.Time // time registered with tailcontrol
LastWrite time.Time // time last packet sent
LastSeen time.Time // last seen to tailcontrol; only present if offline
LastHandshake time.Time // with local wireguard
Online bool // whether node is connected to the control plane
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
RxBytes int64
TxBytes int64
Created time.Time // time registered with tailcontrol
LastWrite time.Time // time last packet sent
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.
// Active is whether the node was recently active. The
// definition is somewhat undefined but has historically and
@@ -243,6 +242,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if v := st.UserID; v != 0 {
e.UserID = v
}
if v := st.TailAddrDeprecated; v != "" {
e.TailAddrDeprecated = v
}
if v := st.TailscaleIPs; v != nil {
e.TailscaleIPs = v
}
@@ -273,9 +275,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if v := st.LastWrite; !v.IsZero() {
e.LastWrite = v
}
if st.Online {
e.Online = true
}
if st.InNetworkMap {
e.InNetworkMap = true
}
@@ -291,9 +290,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if st.ExitNode {
e.ExitNode = true
}
if st.ExitNodeOption {
e.ExitNodeOption = true
}
if st.ShareeNode {
e.ShareeNode = true
}

View File

@@ -12,6 +12,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -19,6 +20,7 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
"inet.af/netaddr"
@@ -26,6 +28,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netknob"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -373,25 +376,6 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(fts)
}
// serveFilePut sends a file to another node.
//
// It's sometimes possible for clients to do this themselves, without
// tailscaled, except in the case of tailscaled running in
// userspace-networking ("netstack") mode, in which case tailscaled
// needs to a do a netstack dial out.
//
// Instead, the CLI also goes through tailscaled so it doesn't need to be
// aware of the network mode in use.
//
// macOS/iOS have always used this localapi method to simplify the GUI
// clients.
//
// The Windows client currently (2021-11-30) uses the peerapi (/v0/put/)
// directly, as the Windows GUI always runs in tun mode anyway.
//
// URL format:
//
// * PUT /localapi/v0/file-put/:stableID/:escaped-filename
func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "file access denied", http.StatusForbidden)
@@ -439,7 +423,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
outReq.ContentLength = r.ContentLength
rp := httputil.NewSingleHostReverseProxy(dstURL)
rp.Transport = h.b.Dialer().PeerAPITransport()
rp.Transport = getDialPeerTransport(h.b)
rp.ServeHTTP(w, outReq)
}
@@ -473,6 +457,26 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
e.Encode(h.b.DERPMap())
}
var dialPeerTransportOnce struct {
sync.Once
v *http.Transport
}
func getDialPeerTransport(b *ipnlocal.LocalBackend) *http.Transport {
dialPeerTransportOnce.Do(func() {
t := http.DefaultTransport.(*http.Transport).Clone()
t.Dial = nil
dialer := net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
Control: b.PeerDialControlFunc(),
}
t.DialContext = dialer.DialContext
dialPeerTransportOnce.v = t
})
return dialPeerTransportOnce.v
}
func defBool(a string, def bool) bool {
if a == "" {
return def

View File

@@ -14,8 +14,7 @@ import (
// system (a version.OS value) is an interesting enough port to report
// to our peer nodes for discovery purposes.
func IsInterestingService(s tailcfg.Service, os string) bool {
switch s.Proto {
case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
if s.Proto == "peerapi4" || s.Proto == "peerapi6" {
return true
}
if s.Proto != tailcfg.TCP {

View File

@@ -9,7 +9,6 @@ package filelogger
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
@@ -27,30 +26,30 @@ const (
maxFiles = 50
)
// New returns a Writer that appends to local disk log
// New returns a logf wrapper that appends to local disk log
// files on Windows, rotating old log files as needed to stay under
// file count & byte limits.
func New(fileBasePrefix, logID string, inner *log.Logger) io.Writer {
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
if runtime.GOOS != "windows" {
panic("not yet supported on any platform except Windows")
}
if inner == nil {
panic("nil inner logger")
if logf == nil {
panic("nil logf")
}
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
if err := os.MkdirAll(dir, 0700); err != nil {
inner.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return inner.Writer()
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return logf
}
inner.Printf("local disk logdir: %v", dir)
logf("local disk logdir: %v", dir)
lfw := &logFileWriter{
fileBasePrefix: fileBasePrefix,
logID: logID,
dir: dir,
wrappedLogf: inner.Printf,
wrappedLogf: logf,
}
return logger.FuncWriter(lfw.Logf)
return lfw.Logf
}
// logFileWriter is the state for the log writer & rotator.

View File

@@ -525,7 +525,7 @@ func New(collection string) *Policy {
}
lw := logtail.NewLogger(c, log.Printf)
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(maybeWrapForPlatform(lw, cmdName, newc.PublicID.String()))
log.SetOutput(lw)
log.Printf("Program starting: v%v, Go %v: %#v",
version.Long,

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package logpolicy
import (
"io"
)
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
return lw
}

View File

@@ -1,26 +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 logpolicy
import (
"io"
"log"
"golang.org/x/sys/windows/svc"
"tailscale.com/log/filelogger"
)
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
if cmdName != "tailscaled" {
return lw
}
isSvc, err := svc.IsWindowsService()
if err != nil || !isSvc {
return lw
}
return filelogger.New("tailscale-service", logID, log.New(lw, "", 0))
}

View File

@@ -18,6 +18,7 @@ import (
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
)
@@ -173,6 +174,10 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
return readResolv(&conf)
}
func (m *resolvconfManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m *resolvconfManager) Close() error {
if err := m.deleteTailscaleConfig(); err != nil {
return err

View File

@@ -7,7 +7,6 @@ package dns
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"fmt"
"io"
@@ -17,9 +16,9 @@ import (
"path/filepath"
"runtime"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -134,18 +133,10 @@ func isResolvedRunning() bool {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
// is-active exits with code 3 if the service is not active.
return err == nil
}
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
func restartResolved() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
return err == nil
}
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
@@ -193,6 +184,24 @@ func (m *directManager) readResolvFile(path string) (OSConfig, error) {
return readResolv(bytes.NewReader(b))
}
func (m *directManager) GetExitNodeForwardResolver() (ret []dnstype.Resolver, retErr error) {
for _, filename := range []string{backupConf, resolvConf} {
if oc, err := m.readResolvFile(filename); err == nil {
for _, ip := range oc.Nameservers {
if ip != netaddr.IPv4(100, 100, 100, 100) {
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
}
}
if len(ret) > 0 {
return ret, nil
}
} else if !os.IsNotExist(err) && retErr == nil {
retErr = err
}
}
return nil, retErr
}
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
// tailscale-managed file.
func (m *directManager) ownedByTailscale() (bool, error) {
@@ -404,12 +413,7 @@ func (m *directManager) Close() error {
}
if isResolvedRunning() && !runningAsGUIDesktopUser() {
m.logf("restarting systemd-resolved...")
if err := restartResolved(); err != nil {
m.logf("restart of systemd-resolved failed: %v", err)
} else {
m.logf("restarted systemd-resolved")
}
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
}
return nil

View File

@@ -26,8 +26,8 @@ func TestParseIni(t *testing.T) {
[network] # trailing comment
generateResolvConf = false # trailing comment`,
want: map[string]map[string]string{
"automount": {"enabled": "true", "root": "/mnt/"},
"network": {"generateResolvConf": "false"},
"automount": map[string]string{"enabled": "true", "root": "/mnt/"},
"network": map[string]string{"generateResolvConf": "false"},
},
},
}

View File

@@ -13,7 +13,6 @@ import (
"tailscale.com/health"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
@@ -40,14 +39,11 @@ type Manager struct {
}
// NewManagers created a new manager from the given config.
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager {
if dialer == nil {
panic("nil Dialer")
}
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, linkSel resolver.ForwardLinkSelector) *Manager {
logf = logger.WithPrefix(logf, "dns: ")
m := &Manager{
logf: logf,
resolver: resolver.New(logf, linkMon, linkSel, dialer),
resolver: resolver.New(logf, linkMon, linkSel),
os: oscfg,
}
m.logf("using %T", m.os)
@@ -66,6 +62,11 @@ func (m *Manager) Set(cfg Config) error {
if err != nil {
return err
}
exitNodeBackupResolvers, err := m.os.GetExitNodeForwardResolver()
if err != nil {
return err
}
rcfg.ExitNodeBackupResolvers = exitNodeBackupResolvers
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w)
@@ -234,7 +235,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
logf("creating dns cleanup: %v", err)
return
}
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
dns := NewManager(logf, oscfg, nil, nil)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"inet.af/netaddr"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
)
@@ -46,6 +45,10 @@ func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
return c.BaseConfig, nil
}
func (c *fakeOSConfigurator) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(c)
}
func (c *fakeOSConfigurator) Close() error { return nil }
func TestManager(t *testing.T) {
@@ -214,6 +217,7 @@ func TestManager(t *testing.T) {
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -250,6 +254,7 @@ func TestManager(t *testing.T) {
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -294,7 +299,8 @@ func TestManager(t *testing.T) {
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -343,7 +349,8 @@ func TestManager(t *testing.T) {
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -399,7 +406,7 @@ func TestManager(t *testing.T) {
SplitDNS: test.split,
BaseConfig: test.bs,
}
m := NewManager(t.Logf, &f, nil, new(tsdial.Dialer), nil)
m := NewManager(t.Logf, &f, nil, nil)
m.resolver.TestOnlySetHook(f.SetResolver)
if err := m.Set(test.in); err != nil {

View File

@@ -17,6 +17,7 @@ import (
"golang.org/x/sys/windows/registry"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -340,6 +341,10 @@ func (m windowsManager) GetBaseConfig() (OSConfig, error) {
}, nil
}
func (m windowsManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
// resolver on the system.
// It's used on Windows 7 to emulate split DNS by trying to figure out

View File

@@ -16,6 +16,7 @@ import (
"github.com/godbus/dbus/v5"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
"tailscale.com/util/endian"
)
@@ -374,6 +375,10 @@ func (m *nmManager) GetBaseConfig() (OSConfig, error) {
return ret, nil
}
func (m *nmManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m *nmManager) Close() error {
// No need to do anything on close, NetworkManager will delete our
// settings when the tailscale interface goes away.

View File

@@ -4,14 +4,21 @@
package dns
import "tailscale.com/types/dnstype"
type noopManager struct{}
var _ OSConfigurator = noopManager{}
func (m noopManager) SetDNS(OSConfig) error { return nil }
func (m noopManager) SupportsSplitDNS() bool { return false }
func (m noopManager) Close() error { return nil }
func (m noopManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m noopManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return nil, nil
}
func NewNoopManager() (noopManager, error) {
return noopManager{}, nil

View File

@@ -12,6 +12,8 @@ import (
"fmt"
"os/exec"
"strings"
"tailscale.com/types/dnstype"
)
// openresolvManager manages DNS configuration using the openresolv
@@ -90,6 +92,10 @@ func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
return readResolv(&buf)
}
func (m openresolvManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m openresolvManager) Close() error {
return m.deleteTailscaleConfig()
}

View File

@@ -8,6 +8,7 @@ import (
"errors"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
)
@@ -18,20 +19,35 @@ type OSConfigurator interface {
// configuration is removed.
// SetDNS must not be called after Close.
SetDNS(cfg OSConfig) error
// SupportsSplitDNS reports whether the configurator is capable of
// installing a resolver only for specific DNS suffixes. If false,
// the configurator can only set a global resolver.
SupportsSplitDNS() bool
// GetBaseConfig returns the OS's "base" configuration, i.e. the
// resolver settings the OS would use without Tailscale
// contributing any configuration.
// GetBaseConfig must return the tailscale-free base config even
// after SetDNS has been called to set a Tailscale configuration.
// Only works when SupportsSplitDNS=false.
//
// Implementations that don't support getting the base config must
// return ErrGetBaseConfigNotSupported.
GetBaseConfig() (OSConfig, error)
// GetExitNodeForwardResolver returns the resolver(s) that should
// be used as a fallback for the exit node's DNS-over-HTTP peerapi
// to send DNS queries from peers on to, in the case where the tailnet
// doesn't have global DNS servers configured.
//
// For example, on Linux with systemd-resolved, this will
// return 127.0.0.53:53.
//
// On other systems, it'll usually be the value of
// GetBaseConfig.Nameservers.
GetExitNodeForwardResolver() ([]dnstype.Resolver, error)
// Close removes Tailscale-related DNS configuration from the OS.
Close() error
}
@@ -90,3 +106,16 @@ func (a OSConfig) Equal(b OSConfig) bool {
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
func getExitNodeForwardResolverFromBaseConfig(o OSConfigurator) (ret []dnstype.Resolver, retErr error) {
oc, err := o.GetBaseConfig()
if err != nil {
return nil, err
}
for _, ip := range oc.Nameservers {
if ip != netaddr.IPv4(100, 100, 100, 100) {
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
}
}
return ret, nil
}

View File

@@ -19,6 +19,7 @@ import (
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -332,6 +333,10 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m *resolvedManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return []dnstype.Resolver{{Addr: "127.0.0.53:53"}}, nil
}
func (m *resolvedManager) Close() error {
m.cancelSyncer()

View File

@@ -26,7 +26,6 @@ import (
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
@@ -160,8 +159,7 @@ type resolverAndDelay struct {
type forwarder struct {
logf logger.Logf
linkMon *monitor.Mon
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
dialer *tsdial.Dialer
linkSel ForwardLinkSelector
dohSem chan struct{}
ctx context.Context // good until Close
@@ -207,12 +205,11 @@ func maxDoHInFlight(goos string) int {
return 1000
}
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
f := &forwarder{
logf: logger.WithPrefix(logf, "forward: "),
linkMon: linkMon,
linkSel: linkSel,
dialer: dialer,
responses: responses,
dohSem: make(chan struct{}, maxDoHInFlight(runtime.GOOS)),
}
@@ -426,7 +423,8 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
if strings.HasPrefix(rr.name.Addr, "http://") {
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("http:// resolvers not supported yet")
}
if strings.HasPrefix(rr.name.Addr, "https://") {
metricDNSFwdErrorType.Add(1)
@@ -582,9 +580,9 @@ func (f *forwarder) forward(query packet) error {
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
//
// If resolvers is non-empty, it's used explicitly (notably, for exit
// node DNS proxy queries), otherwise f.resolvers is used.
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, resolvers ...resolverAndDelay) error {
// If backupResolvers are specified, they're used in the case that no
// upstreams are available.
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, backupResolvers ...resolverAndDelay) error {
metricDNSFwd.Add(1)
domain, err := nameFromQuery(query.bs)
if err != nil {
@@ -603,12 +601,13 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
clampEDNSSize(query.bs, maxResponseBytes)
resolvers := f.resolvers(domain)
if len(resolvers) == 0 {
resolvers = f.resolvers(domain)
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
return errNoUpstreams
}
resolvers = backupResolvers
}
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
return errNoUpstreams
}
fq := &forwardQuery{

View File

@@ -8,14 +8,11 @@ package resolver
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"os"
"runtime"
"sort"
"strings"
@@ -23,16 +20,12 @@ import (
"sync/atomic"
"time"
"go4.org/mem"
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/wgengine/monitor"
)
@@ -90,6 +83,14 @@ type Config struct {
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []dnsname.FQDN
// ExitNodeBackupResolvers are where the local node when
// acting as an exit node and serving a DNS proxy should
// forward DNS requests to in the case where there are no
// routes found. For example, for Linux systemd-resolved
// machines this is likely 127.0.0.53:53.
// If it's empty, there are no backups and the OS should
// be queried directly using its OS-level DNS APIs.
ExitNodeBackupResolvers []dnstype.Resolver
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting
@@ -193,7 +194,6 @@ func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]dnstype.Resolver) {
type Resolver struct {
logf logger.Logf
linkMon *monitor.Mon // or nil
dialer *tsdial.Dialer // non-nil
saveConfigForTests func(cfg Config) // used in tests to capture resolver config
// forwarder forwards requests to upstream nameservers.
forwarder *forwarder
@@ -210,10 +210,11 @@ type Resolver struct {
wg sync.WaitGroup
// mu guards the following fields from being updated while used.
mu sync.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN
mu sync.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN
exitNodeBackupResolvers []dnstype.Resolver
}
type ForwardLinkSelector interface {
@@ -225,10 +226,7 @@ type ForwardLinkSelector interface {
// New returns a new resolver.
// linkMon optionally specifies a link monitor to use for socket rebinding.
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *Resolver {
if dialer == nil {
panic("nil Dialer")
}
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *Resolver {
r := &Resolver{
logf: logger.WithPrefix(logf, "resolver: "),
linkMon: linkMon,
@@ -237,9 +235,8 @@ func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector, di
closed: make(chan struct{}),
hostToIP: map[dnsname.FQDN][]netaddr.IP{},
ipToHost: map[netaddr.IP]dnsname.FQDN{},
dialer: dialer,
}
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel, dialer)
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel)
return r
}
@@ -265,9 +262,16 @@ func (r *Resolver) SetConfig(cfg Config) error {
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
r.exitNodeBackupResolvers = append([]dnstype.Resolver(nil), cfg.ExitNodeBackupResolvers...)
return nil
}
func (r *Resolver) exitNodeForwardResolvers() []dnstype.Resolver {
r.mu.Lock()
defer r.mu.Unlock()
return r.exitNodeBackupResolvers
}
// Close shuts down the resolver and ensures poll goroutines have exited.
// The Resolver cannot be used again after Close is called.
func (r *Resolver) Close() {
@@ -315,84 +319,27 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error)
}
}
// parseExitNodeQuery parses a DNS request packet.
// It returns nil if it's malformed or lacking a question.
func parseExitNodeQuery(q []byte) *response {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
p.zeroParser()
defer p.zeroParser()
if err := p.parseQuery(q); err != nil {
return nil
}
return p.response()
}
// HandleExitNodeDNSQuery handles a DNS query that arrived from a peer
// via the peerapi's DoH server. This is only used when the local
// node is being an exit node.
//
// The provided allowName callback is whether a DNS query for a name
// (as found by parsing q) is allowed.
//
// In most (all?) cases, err will be nil. A bogus DNS query q will
// still result in a response DNS packet (saying there's a failure)
// and a nil error.
// TODO: figure out if we even need an error result.
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort, allowName func(name string) bool) (res []byte, err error) {
metricDNSExitProxyQuery.Add(1)
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort) (res []byte, err error) {
metricDNSQueryForPeer.Add(1)
ch := make(chan packet, 1)
resp := parseExitNodeQuery(q)
if resp == nil {
return nil, errors.New("bad query")
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
if err == errNoUpstreams {
backup := r.exitNodeForwardResolvers()
if len(backup) > 0 {
var extra []resolverAndDelay
for _, v := range backup {
extra = append(extra, resolverAndDelay{name: v})
}
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, extra...)
}
}
name := resp.Question.Name.String()
if !allowName(name) {
metricDNSExitProxyErrorName.Add(1)
resp.Header.RCode = dns.RCodeRefused
return marshalResponse(resp)
}
switch runtime.GOOS {
default:
return nil, errors.New("unsupported exit node OS")
case "windows":
// TODO: use DnsQueryEx and write to ch.
// See https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsqueryex.
// For now just use the net package:
return handleExitNodeDNSQueryWithNetPkg(ctx, nil, resp)
case "darwin":
// /etc/resolv.conf is a lie and only says one upstream DNS
// but for now that's probably good enough. Later we'll
// want to blend in everything from scutil --dns.
fallthrough
case "linux", "freebsd", "openbsd", "illumos":
nameserver, err := stubResolverForOS()
if err != nil {
r.logf("stubResolverForOS: %v", err)
metricDNSExitProxyErrorResolvConf.Add(1)
return nil, err
}
// TODO: more than 1 resolver from /etc/resolv.conf?
var resolvers []resolverAndDelay
if nameserver == tsaddr.TailscaleServiceIP() {
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
// so avoid the loop through the kernel and just do what we
// would've done anyway. By not passing any resolvers, the forwarder
// will use its default ones from our DNS config.
} else {
resolvers = []resolverAndDelay{{
name: dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
}}
}
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolvers...)
if err != nil {
metricDNSExitProxyErrorForward.Add(1)
return nil, err
}
if err != nil {
metricDNSQueryForPeerError.Add(1)
return nil, err
}
select {
case p, ok := <-ch:
@@ -405,159 +352,6 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
}
}
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
// return a reply (for the ExitDNS DoH service) using the net package's
// native APIs. This is only used on Windows for now.
//
// If resolver is nil, the net.Resolver zero value is used.
//
// response contains the pre-serialized response, which notably
// includes the original question and its header.
func handleExitNodeDNSQueryWithNetPkg(ctx context.Context, resolver *net.Resolver, resp *response) (res []byte, err error) {
if resp.Question.Class != dns.ClassINET {
return nil, errors.New("unsupported class")
}
r := resolver
if r == nil {
r = new(net.Resolver)
}
name := resp.Question.Name.String()
handleError := func(err error) (res []byte, _ error) {
if isGoNoSuchHostError(err) {
resp.Header.RCode = dns.RCodeNameError
return marshalResponse(resp)
}
// TODO: map other errors to RCodeServerFailure?
// Or I guess our caller should do that?
return nil, err
}
resp.Header.RCode = dns.RCodeSuccess // unless changed below
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA:
network := "ip4"
if resp.Question.Type == dns.TypeAAAA {
network = "ip6"
}
ips, err := r.LookupIP(ctx, network, name)
if err != nil {
return handleError(err)
}
for _, stdIP := range ips {
if ip, ok := netaddr.FromStdIP(stdIP); ok {
resp.IPs = append(resp.IPs, ip)
}
}
case dns.TypeTXT:
strs, err := r.LookupTXT(ctx, name)
if err != nil {
return handleError(err)
}
resp.TXT = strs
case dns.TypePTR:
ipStr, ok := unARPA(name)
if !ok {
// TODO: is this RCodeFormatError?
return nil, errors.New("bogus PTR name")
}
addrs, err := r.LookupAddr(ctx, ipStr)
if err != nil {
return handleError(err)
}
if len(addrs) > 0 {
resp.Name, _ = dnsname.ToFQDN(addrs[0])
}
case dns.TypeCNAME:
cname, err := r.LookupCNAME(ctx, name)
if err != nil {
return handleError(err)
}
resp.CNAME = cname
case dns.TypeSRV:
// Thanks, Go: "To accommodate services publishing SRV
// records under non-standard names, if both service
// and proto are empty strings, LookupSRV looks up
// name directly."
_, srvs, err := r.LookupSRV(ctx, "", "", name)
if err != nil {
return handleError(err)
}
resp.SRVs = srvs
case dns.TypeNS:
nss, err := r.LookupNS(ctx, name)
if err != nil {
return handleError(err)
}
resp.NSs = nss
default:
return nil, fmt.Errorf("unsupported record type %v", resp.Question.Type)
}
return marshalResponse(resp)
}
func isGoNoSuchHostError(err error) bool {
if de, ok := err.(*net.DNSError); ok {
return de.IsNotFound
}
return false
}
type resolvConfCache struct {
mod time.Time
size int64
ip netaddr.IP
// TODO: inode/dev?
}
// resolvConfCacheValue contains the most recent stat metadata and parsed
// version of /etc/resolv.conf.
var resolvConfCacheValue atomic.Value // of resolvConfCache
var errEmptyResolvConf = errors.New("resolv.conf has no nameservers")
// stubResolverForOS returns the IP address of the first nameserver in
// /etc/resolv.conf.
func stubResolverForOS() (ip netaddr.IP, err error) {
fi, err := os.Stat("/etc/resolv.conf")
if err != nil {
return netaddr.IP{}, err
}
cur := resolvConfCache{
mod: fi.ModTime(),
size: fi.Size(),
}
if c, ok := resolvConfCacheValue.Load().(resolvConfCache); ok && c.mod == cur.mod && c.size == cur.size {
return c.ip, nil
}
err = lineread.File("/etc/resolv.conf", func(line []byte) error {
if !ip.IsZero() {
return nil
}
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' {
return nil
}
if mem.HasPrefix(mem.B(line), mem.S("nameserver ")) {
s := strings.TrimSpace(strings.TrimPrefix(string(line), "nameserver "))
ip, err = netaddr.ParseIP(s)
return err
}
return nil
})
if err != nil {
return netaddr.IP{}, err
}
if !ip.IsValid() {
return netaddr.IP{}, errEmptyResolvConf
}
cur.ip = ip
resolvConfCacheValue.Store(cur)
return ip, nil
}
// resolveLocal returns an IP for the given domain, if domain is in
// the local hosts map and has an IP corresponding to the requested
// typ (A, AAAA, ALL).
@@ -705,27 +499,10 @@ func (r *Resolver) handleQuery(pkt packet) {
type response struct {
Header dns.Header
Question dns.Question
// Name is the response to a PTR query.
Name dnsname.FQDN
// IP and IPs are the responses to an A, AAAA, or ALL query.
// Either/both/neither can be populated.
IP netaddr.IP
IPs []netaddr.IP
// TXT is the response to a TXT query.
// Each one is its own RR with one string.
TXT []string
// CNAME is the response to a CNAME query.
CNAME string
// SRVs are the responses to a SRV query.
SRVs []*net.SRV
// NSs are the responses to an NS query.
NSs []*net.NS
// IP is the response to an A, AAAA, or ALL query.
IP netaddr.IP
}
var dnsParserPool = &sync.Pool{
@@ -756,7 +533,6 @@ func (p *dnsParser) zeroParser() { p.parser = dns.Parser{} }
// p.Question.
func (p *dnsParser) parseQuery(query []byte) error {
defer p.zeroParser()
p.zeroParser()
var err error
p.Header, err = p.parser.Start(query)
if err != nil {
@@ -801,16 +577,6 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
return builder.AAAAResource(answerHeader, answer)
}
func marshalIP(name dns.Name, ip netaddr.IP, builder *dns.Builder) error {
if ip.Is4() {
return marshalARecord(name, ip, builder)
}
if ip.Is6() {
return marshalAAAARecord(name, ip, builder)
}
return nil
}
// marshalPTRRecord serializes a PTR record into an active builder.
// The caller may continue using the builder following the call.
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
@@ -830,83 +596,6 @@ func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builde
return builder.PTRResource(answerHeader, answer)
}
func marshalTXT(queryName dns.Name, txts []string, builder *dns.Builder) error {
for _, txt := range txts {
if err := builder.TXTResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeTXT,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.TXTResource{
TXT: []string{txt},
}); err != nil {
return err
}
}
return nil
}
func marshalCNAME(queryName dns.Name, cname string, builder *dns.Builder) error {
if cname == "" {
return nil
}
name, err := dns.NewName(cname)
if err != nil {
return err
}
return builder.CNAMEResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeCNAME,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.CNAMEResource{
CNAME: name,
})
}
func marshalNS(queryName dns.Name, nss []*net.NS, builder *dns.Builder) error {
for _, ns := range nss {
name, err := dns.NewName(ns.Host)
if err != nil {
return err
}
err = builder.NSResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeNS,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.NSResource{NS: name})
if err != nil {
return err
}
}
return nil
}
func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error {
for _, s := range srvs {
srvName, err := dns.NewName(s.Target)
if err != nil {
return err
}
err = builder.SRVResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeSRV,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.SRVResource{
Target: srvName,
Priority: s.Priority,
Port: s.Port,
Weight: s.Weight,
})
if err != nil {
return err
}
}
return nil
}
// marshalResponse serializes the DNS response into a new buffer.
func marshalResponse(resp *response) ([]byte, error) {
resp.Header.Response = true
@@ -917,14 +606,6 @@ func marshalResponse(resp *response) ([]byte, error) {
builder := dns.NewBuilder(nil, resp.Header)
// TODO(bradfitz): I'm not sure why this wasn't enabled
// before, but for now (2021-12-09) enable it at least when
// there's more than 1 record (which was never the case
// before), where it really helps.
if len(resp.IPs) > 1 {
builder.EnableCompression()
}
isSuccess := resp.Header.RCode == dns.RCodeSuccess
if resp.Question.Type != 0 || isSuccess {
@@ -951,24 +632,13 @@ func marshalResponse(resp *response) ([]byte, error) {
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
if err := marshalIP(resp.Question.Name, resp.IP, &builder); err != nil {
return nil, err
}
for _, ip := range resp.IPs {
if err := marshalIP(resp.Question.Name, ip, &builder); err != nil {
return nil, err
}
if resp.IP.Is4() {
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
} else if resp.IP.Is6() {
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
}
case dns.TypePTR:
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
case dns.TypeTXT:
err = marshalTXT(resp.Question.Name, resp.TXT, &builder)
case dns.TypeCNAME:
err = marshalCNAME(resp.Question.Name, resp.CNAME, &builder)
case dns.TypeSRV:
err = marshalSRV(resp.Question.Name, resp.SRVs, &builder)
case dns.TypeNS:
err = marshalNS(resp.Question.Name, resp.NSs, &builder)
}
if err != nil {
return nil, err
@@ -1150,37 +820,6 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
return marshalResponse(resp)
}
// unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc.
func unARPA(a string) (ipStr string, ok bool) {
const suf4 = ".in-addr.arpa."
if strings.HasSuffix(a, suf4) {
s := strings.TrimSuffix(a, suf4)
// Parse and reverse octets.
ip, err := netaddr.ParseIP(s)
if err != nil || !ip.Is4() {
return "", false
}
a4 := ip.As4()
return netaddr.IPv4(a4[3], a4[2], a4[1], a4[0]).String(), true
}
const suf6 = ".ip6.arpa."
if len(a) == len("e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.") &&
strings.HasSuffix(a, suf6) {
var hx [32]byte
var a16 [16]byte
for i := range hx {
hx[31-i] = a[i*2]
if a[i*2+1] != '.' {
return "", false
}
}
hex.Decode(a16[:], hx[:])
return netaddr.IPFrom16(a16).String(), true
}
return "", false
}
var (
metricDNSQueryLocal = clientmetric.NewCounter("dns_query_local")
metricDNSQueryErrorClosed = clientmetric.NewCounter("dns_query_local_error_closed")
@@ -1193,10 +832,8 @@ var (
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
metricDNSExitProxyQuery = clientmetric.NewCounter("dns_exit_node_query")
metricDNSExitProxyErrorName = clientmetric.NewCounter("dns_exit_node_error_name")
metricDNSExitProxyErrorForward = clientmetric.NewCounter("dns_exit_node_error_forward")
metricDNSExitProxyErrorResolvConf = clientmetric.NewCounter("dns_exit_node_error_resolvconf")
metricDNSQueryForPeer = clientmetric.NewCounter("dns_query_peerapi")
metricDNSQueryForPeerError = clientmetric.NewCounter("dns_query_peerapi_error")
metricDNSFwd = clientmetric.NewCounter("dns_query_fwd")
metricDNSFwdDropBonjour = clientmetric.NewCounter("dns_query_fwd_drop_bonjour")

View File

@@ -6,7 +6,6 @@ package resolver
import (
"fmt"
"net"
"strings"
"testing"
@@ -180,129 +179,6 @@ var resolveToNXDOMAIN = dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg)
w.WriteMsg(m)
})
// weirdoGoCNAMEHandler returns a DNS handler that satisfies
// Go's weird Resolver.LookupCNAME (read its godoc carefully!).
//
// This doesn't even return a CNAME record, because that's not
// what Go looks for.
func weirdoGoCNAMEHandler(target string) dns.HandlerFunc {
return func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
question := req.Question[0]
switch question.Qtype {
case dns.TypeA:
m.Answer = append(m.Answer, &dns.CNAME{
Hdr: dns.RR_Header{
Name: target,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 600,
},
Target: target,
})
case dns.TypeAAAA:
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: target,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: 600,
},
AAAA: net.ParseIP("1::2"),
})
}
w.WriteMsg(m)
}
}
// dnsHandler returns a handler that replies with the answers/options
// provided.
//
// Types supported: netaddr.IP.
func dnsHandler(answers ...interface{}) dns.HandlerFunc {
return func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
if len(req.Question) != 1 {
panic("not a single-question request")
}
m.RecursionAvailable = true // to stop net package's errLameReferral on empty replies
question := req.Question[0]
for _, a := range answers {
switch a := a.(type) {
default:
panic(fmt.Sprintf("unsupported dnsHandler arg %T", a))
case netaddr.IP:
ip := a
if ip.Is4() {
m.Answer = append(m.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: ip.IPAddr().IP,
})
} else if ip.Is6() {
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
},
AAAA: ip.IPAddr().IP,
})
}
case dns.PTR:
ptr := a
ptr.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &ptr)
case dns.CNAME:
c := a
c.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 600,
}
m.Answer = append(m.Answer, &c)
case dns.TXT:
txt := a
txt.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &txt)
case dns.SRV:
srv := a
srv.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &srv)
case dns.NS:
rr := a
rr.Hdr = dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
}
m.Answer = append(m.Answer, &rr)
}
}
w.WriteMsg(m)
}
}
func serveDNS(tb testing.TB, addr string, records ...interface{}) *dns.Server {
if len(records)%2 != 0 {
panic("must have an even number of record values")

View File

@@ -6,25 +6,18 @@ package resolver
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
miekdns "github.com/miekg/dns"
"golang.org/x/net/dns/dnsmessage"
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/net/tsdial"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
@@ -41,16 +34,14 @@ var (
var dnsCfg = Config{
Hosts: map[dnsname.FQDN][]netaddr.IP{
"test1.ipn.dev.": {testipv4},
"test2.ipn.dev.": {testipv6},
"test1.ipn.dev.": []netaddr.IP{testipv4},
"test2.ipn.dev.": []netaddr.IP{testipv6},
},
LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."},
}
const noEdns = 0
const dnsHeaderLen = 12
func dnspacket(domain dnsname.FQDN, tp dns.Type, ednsSize uint16) []byte {
var dnsHeader dns.Header
question := dns.Question{
@@ -317,7 +308,7 @@ func TestRDNSNameToIPv6(t *testing.T) {
}
func newResolver(t testing.TB) *Resolver {
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */, new(tsdial.Dialer))
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */)
}
func TestResolveLocal(t *testing.T) {
@@ -1071,7 +1062,7 @@ func TestForwardLinkSelection(t *testing.T) {
return "special"
}
return ""
}), new(tsdial.Dialer))
}))
// Test non-special IP.
if got, err := fwd.packetListener(netaddr.IP{}); err != nil {
@@ -1101,383 +1092,3 @@ func TestForwardLinkSelection(t *testing.T) {
type linkSelFunc func(ip netaddr.IP) string
func (f linkSelFunc) PickLink(ip netaddr.IP) string { return f(ip) }
func TestHandleExitNodeDNSQueryWithNetPkg(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows; waiting for golang.org/issue/33097")
}
records := []interface{}{
"no-records.test.",
dnsHandler(),
"one-a.test.",
dnsHandler(netaddr.MustParseIP("1.2.3.4")),
"two-a.test.",
dnsHandler(netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("5.6.7.8")),
"one-aaaa.test.",
dnsHandler(netaddr.MustParseIP("1::2")),
"two-aaaa.test.",
dnsHandler(netaddr.MustParseIP("1::2"), netaddr.MustParseIP("3::4")),
"nx-domain.test.",
resolveToNXDOMAIN,
"4.3.2.1.in-addr.arpa.",
dnsHandler(miekdns.PTR{Ptr: "foo.com."}),
"cname.test.",
weirdoGoCNAMEHandler("the-target.foo."),
"txt.test.",
dnsHandler(
miekdns.TXT{Txt: []string{"txt1=one"}},
miekdns.TXT{Txt: []string{"txt2=two"}},
miekdns.TXT{Txt: []string{"txt3=three"}},
),
"srv.test.",
dnsHandler(
miekdns.SRV{
Priority: 1,
Weight: 2,
Port: 3,
Target: "foo.com.",
},
miekdns.SRV{
Priority: 4,
Weight: 5,
Port: 6,
Target: "bar.com.",
},
),
"ns.test.",
dnsHandler(miekdns.NS{Ns: "ns1.foo."}, miekdns.NS{Ns: "ns2.bar."}),
}
v4server := serveDNS(t, "127.0.0.1:0", records...)
defer v4server.Shutdown()
// backendResolver is the resolver between
// handleExitNodeDNSQueryWithNetPkg and its upstream resolver,
// which in this test's case is the miekg/dns test DNS server
// (v4server).
backResolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "udp", v4server.PacketConn.LocalAddr().String())
},
}
t.Run("no_such_host", func(t *testing.T) {
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
Header: dnsmessage.Header{
ID: 123,
Response: true,
OpCode: 0, // query
},
Question: dnsmessage.Question{
Name: dnsmessage.MustNewName("nx-domain.test."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
})
if err != nil {
t.Fatal(err)
}
if len(res) < dnsHeaderLen {
t.Fatal("short reply")
}
rcode := dns.RCode(res[3] & 0x0f)
if rcode != dns.RCodeNameError {
t.Errorf("RCode = %v; want dns.RCodeNameError", rcode)
t.Logf("Response was: %q", res)
}
})
matchPacked := func(want string) func(t testing.TB, got []byte) {
return func(t testing.TB, got []byte) {
if string(got) == want {
return
}
t.Errorf("unexpected reply.\n got: %q\nwant: %q\n", got, want)
t.Errorf("\nin hex:\n got: % 2x\nwant: % 2x\n", got, want)
}
}
tests := []struct {
Type dnsmessage.Type
Name string
Check func(t testing.TB, got []byte)
}{
{
Type: dnsmessage.TypeA,
Name: "one-a.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05one-a\x04test\x00\x00\x01\x00\x01\x05one-a\x04test\x00\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04"),
},
{
Type: dnsmessage.TypeA,
Name: "two-a.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x05two-a\x04test\x00\x00\x01\x00\x01\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x05\x06\a\b"),
},
{
Type: dnsmessage.TypeAAAA,
Name: "one-aaaa.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\bone-aaaa\x04test\x00\x00\x1c\x00\x01\bone-aaaa\x04test\x00\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"),
},
{
Type: dnsmessage.TypeAAAA,
Name: "two-aaaa.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\btwo-aaaa\x04test\x00\x00\x1c\x00\x01\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04"),
},
{
Type: dnsmessage.TypePTR,
Name: "4.3.2.1.in-addr.arpa.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x00\x00\x02X\x00\t\x03foo\x03com\x00"),
},
{
Type: dnsmessage.TypeCNAME,
Name: "cname.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05cname\x04test\x00\x00\x05\x00\x01\x05cname\x04test\x00\x00\x05\x00\x01\x00\x00\x02X\x00\x10\nthe-target\x03foo\x00"),
},
// No records of various types
{
Type: dnsmessage.TypeA,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x01\x00\x01"),
},
{
Type: dnsmessage.TypeAAAA,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x1c\x00\x01"),
},
{
Type: dnsmessage.TypeCNAME,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x05\x00\x01"),
},
{
Type: dnsmessage.TypeSRV,
Name: "no-records.test.",
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00!\x00\x01"),
},
{
Type: dnsmessage.TypeTXT,
Name: "txt.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x03\x00\x00\x00\x00\x03txt\x04test\x00\x00\x10\x00\x01\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt1=one\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt2=two\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\v\ntxt3=three"),
},
{
Type: dnsmessage.TypeSRV,
Name: "srv.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x03srv\x04test\x00\x00!\x00\x01\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x01\x00\x02\x00\x03\x03foo\x03com\x00\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x04\x00\x05\x00\x06\x03bar\x03com\x00"),
},
{
Type: dnsmessage.TypeNS,
Name: "ns.test.",
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x02ns\x04test\x00\x00\x02\x00\x01\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns1\x03foo\x00\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns2\x03bar\x00"),
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v_%v", tt.Type, strings.Trim(tt.Name, ".")), func(t *testing.T) {
got, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
Header: dnsmessage.Header{
ID: 123,
Response: true,
OpCode: 0, // query
},
Question: dnsmessage.Question{
Name: dnsmessage.MustNewName(tt.Name),
Type: tt.Type,
Class: dnsmessage.ClassINET,
},
})
if err != nil {
t.Fatal(err)
}
if len(got) < dnsHeaderLen {
t.Errorf("short record")
}
if tt.Check != nil {
tt.Check(t, got)
if t.Failed() {
t.Errorf("Got: %q\nIn hex: % 02x", got, got)
}
}
})
}
wrapRes := newWrapResolver(backResolver)
ctx := context.Background()
t.Run("wrap_ip_a", func(t *testing.T) {
ips, err := wrapRes.LookupIP(ctx, "ip", "two-a.test.")
if err != nil {
t.Fatal(err)
}
if got, want := ips, []net.IP{
net.ParseIP("1.2.3.4").To4(),
net.ParseIP("5.6.7.8").To4(),
}; !reflect.DeepEqual(got, want) {
t.Errorf("LookupIP = %v; want %v", got, want)
}
})
t.Run("wrap_ip_aaaa", func(t *testing.T) {
ips, err := wrapRes.LookupIP(ctx, "ip", "two-aaaa.test.")
if err != nil {
t.Fatal(err)
}
if got, want := ips, []net.IP{
net.ParseIP("1::2"),
net.ParseIP("3::4"),
}; !reflect.DeepEqual(got, want) {
t.Errorf("LookupIP(v6) = %v; want %v", got, want)
}
})
t.Run("wrap_ip_nx", func(t *testing.T) {
ips, err := wrapRes.LookupIP(ctx, "ip", "nx-domain.test.")
if !isGoNoSuchHostError(err) {
t.Errorf("no NX domain = (%v, %v); want no host error", ips, err)
}
})
t.Run("wrap_srv", func(t *testing.T) {
_, srvs, err := wrapRes.LookupSRV(ctx, "", "", "srv.test.")
if err != nil {
t.Fatal(err)
}
if got, want := srvs, []*net.SRV{
{
Target: "foo.com.",
Priority: 1,
Weight: 2,
Port: 3,
},
{
Target: "bar.com.",
Priority: 4,
Weight: 5,
Port: 6,
},
}; !reflect.DeepEqual(got, want) {
jgot, _ := json.Marshal(got)
jwant, _ := json.Marshal(want)
t.Errorf("SRV = %s; want %s", jgot, jwant)
}
})
t.Run("wrap_txt", func(t *testing.T) {
txts, err := wrapRes.LookupTXT(ctx, "txt.test.")
if err != nil {
t.Fatal(err)
}
if got, want := txts, []string{"txt1=one", "txt2=two", "txt3=three"}; !reflect.DeepEqual(got, want) {
t.Errorf("TXT = %q; want %q", got, want)
}
})
t.Run("wrap_ns", func(t *testing.T) {
nss, err := wrapRes.LookupNS(ctx, "ns.test.")
if err != nil {
t.Fatal(err)
}
if got, want := nss, []*net.NS{
{Host: "ns1.foo."},
{Host: "ns2.bar."},
}; !reflect.DeepEqual(got, want) {
jgot, _ := json.Marshal(got)
jwant, _ := json.Marshal(want)
t.Errorf("NS = %s; want %s", jgot, jwant)
}
})
}
// newWrapResolver returns a resolver that uses r (via handleExitNodeDNSQueryWithNetPkg)
// to make DNS requests.
func newWrapResolver(r *net.Resolver) *net.Resolver {
if runtime.GOOS == "windows" {
panic("doesn't work on Windows") // golang.org/issue/33097
}
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return &wrapResolverConn{ctx: ctx, r: r}, nil
},
}
}
type wrapResolverConn struct {
ctx context.Context
r *net.Resolver
buf bytes.Buffer
}
var _ net.PacketConn = (*wrapResolverConn)(nil)
func (*wrapResolverConn) Close() error { return nil }
func (*wrapResolverConn) LocalAddr() net.Addr { return fakeAddr{} }
func (*wrapResolverConn) RemoteAddr() net.Addr { return fakeAddr{} }
func (*wrapResolverConn) SetDeadline(t time.Time) error { return nil }
func (*wrapResolverConn) SetReadDeadline(t time.Time) error { return nil }
func (*wrapResolverConn) SetWriteDeadline(t time.Time) error { return nil }
func (a *wrapResolverConn) Read(p []byte) (n int, err error) {
n, _, err = a.ReadFrom(p)
return
}
func (a *wrapResolverConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = a.buf.Read(p)
return n, fakeAddr{}, err
}
func (a *wrapResolverConn) Write(packet []byte) (n int, err error) {
return a.WriteTo(packet, fakeAddr{})
}
func (a *wrapResolverConn) WriteTo(q []byte, _ net.Addr) (n int, err error) {
resp := parseExitNodeQuery(q)
if resp == nil {
return 0, errors.New("bad query")
}
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), a.r, resp)
if err != nil {
return 0, err
}
a.buf.Write(res)
return len(q), nil
}
type fakeAddr struct{}
func (fakeAddr) Network() string { return "unused" }
func (fakeAddr) String() string { return "unused-todoAddr" }
func TestUnARPA(t *testing.T) {
tests := []struct {
in, want string
}{
{"", ""},
{"bad", ""},
{"4.4.8.8.in-addr.arpa.", "8.8.4.4"},
{".in-addr.arpa.", ""},
{"e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.", "2607:f8b0:400a:80b::200e"},
{".ip6.arpa.", ""},
}
for _, tt := range tests {
got, ok := unARPA(tt.in)
if ok != (got != "") {
t.Errorf("inconsistent results for %q: (%q, %v)", tt.in, got, ok)
}
if got != tt.want {
t.Errorf("unARPA(%q) = %q; want %q", tt.in, got, tt.want)
}
}
}

View File

@@ -1,56 +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 dns
// This code is only used in Windows builds, but is in an
// OS-independent file so tests can run all the time.
import (
"bytes"
"encoding/binary"
"unicode/utf16"
)
// maybeUnUTF16 tries to detect whether bs contains UTF-16, and if so
// translates it to regular UTF-8.
//
// Some of wsl.exe's output get printed as UTF-16, which breaks a
// bunch of things. Try to detect this by looking for a zero byte in
// the first few bytes of output (which will appear if any of those
// codepoints are basic ASCII - very likely). From that we can infer
// that UTF-16 is being printed, and the byte order in use, and we
// decode that back to UTF-8.
//
// https://github.com/microsoft/WSL/issues/4607
func maybeUnUTF16(bs []byte) []byte {
if len(bs)%2 != 0 {
// Can't be complete UTF-16.
return bs
}
checkLen := 20
if len(bs) < checkLen {
checkLen = len(bs)
}
zeroOff := bytes.IndexByte(bs[:checkLen], 0)
if zeroOff == -1 {
return bs
}
// We assume wsl.exe is trying to print an ASCII codepoint,
// meaning the zero byte is in the upper 8 bits of the
// codepoint. That means we can use the zero's byte offset to
// work out if we're seeing little-endian or big-endian
// UTF-16.
var endian binary.ByteOrder = binary.LittleEndian
if zeroOff%2 == 0 {
endian = binary.BigEndian
}
var u16 []uint16
for i := 0; i < len(bs); i += 2 {
u16 = append(u16, endian.Uint16(bs[i:]))
}
return []byte(string(utf16.Decode(u16)))
}

View File

@@ -1,25 +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 dns
import "testing"
func TestMaybeUnUTF16(t *testing.T) {
tests := []struct {
in string
want string
}{
{"abc", "abc"}, // UTF-8
{"a\x00b\x00c\x00", "abc"}, // UTF-16-LE
{"\x00a\x00b\x00c", "abc"}, // UTF-16-BE
}
for _, test := range tests {
got := string(maybeUnUTF16([]byte(test.in)))
if got != test.want {
t.Errorf("maybeUnUTF16(%q) = %q, want %q", test.in, got, test.want)
}
}
}

View File

@@ -6,13 +6,13 @@ package dns
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"strings"
"syscall"
"unicode/utf16"
"golang.org/x/sys/windows"
"tailscale.com/types/logger"
@@ -26,7 +26,29 @@ func wslDistros() ([]string, error) {
return nil, fmt.Errorf("%v: %q", err, string(b))
}
lines := strings.Split(string(b), "\n")
// The first line of output is a WSL header. E.g.
//
// C:\tsdev>wsl.exe -l
// Windows Subsystem for Linux Distributions:
// Ubuntu-20.04 (Default)
//
// We can skip it by passing '-q', but here we put it to work.
// It turns out wsl.exe -l is broken, and outputs UTF-16 names
// that nothing can read. (Try `wsl.exe -l | more`.)
// So we look at the header to see if it's UTF-16.
// If so, we run the rest through a UTF-16 parser.
//
// https://github.com/microsoft/WSL/issues/4607
var output string
if bytes.HasPrefix(b, []byte("W\x00i\x00n\x00d\x00o\x00w\x00s\x00")) {
output, err = decodeUTF16(b)
if err != nil {
return nil, fmt.Errorf("failed to decode wsl.exe -l output %q: %v", b, err)
}
} else {
output = string(b)
}
lines := strings.Split(output, "\n")
if len(lines) < 1 {
return nil, nil
}
@@ -44,6 +66,19 @@ func wslDistros() ([]string, error) {
return distros, nil
}
func decodeUTF16(b []byte) (string, error) {
if len(b) == 0 {
return "", nil
} else if len(b)%2 != 0 {
return "", fmt.Errorf("decodeUTF16: invalid length %d", len(b))
}
var u16 []uint16
for i := 0; i < len(b); i += 2 {
u16 = append(u16, uint16(b[i])+(uint16(b[i+1])<<8))
}
return string(utf16.Decode(u16)), nil
}
// wslManager is a DNS manager for WSL2 linux distributions.
// It configures /etc/wsl.conf and /etc/resolv.conf.
type wslManager struct {
@@ -158,8 +193,7 @@ func (fs wslFS) Truncate(name string) error { return fs.WriteFile(name, nil, 064
func (fs wslFS) ReadFile(name string) ([]byte, error) {
b, err := wslCombinedOutput(fs.cmd("cat", "--", name))
var ee *exec.ExitError
if errors.As(err, &ee) && ee.ExitCode() == 1 {
if ee, _ := err.(*exec.ExitError); ee != nil && ee.ExitCode() == 1 {
return nil, os.ErrNotExist
}
return b, err
@@ -191,10 +225,7 @@ func wslCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
cmd.Stdout = buf
cmd.Stderr = buf
err := wslRun(cmd)
if err != nil {
return nil, err
}
return maybeUnUTF16(buf.Bytes()), nil
return buf.Bytes(), err
}
func wslRun(cmd *exec.Cmd) (err error) {

View File

@@ -1,314 +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 dnscache
import (
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/golang/groupcache/lru"
"golang.org/x/net/dns/dnsmessage"
)
// MessageCache is a cache that works at the DNS message layer,
// with its cache keyed on a DNS wire-level question, and capable
// of replying to DNS messages.
//
// Its zero value is ready for use with a default cache size.
// Use SetMaxCacheSize to specify the cache size.
//
// It's safe for concurrent use.
type MessageCache struct {
// Clock is a clock, for testing.
// If nil, time.Now is used.
Clock func() time.Time
mu sync.Mutex
cacheSizeSet int // 0 means default
cache lru.Cache // msgQ => *msgCacheValue
}
func (c *MessageCache) now() time.Time {
if c.Clock != nil {
return c.Clock()
}
return time.Now()
}
// SetMaxCacheSize sets the maximum number of DNS cache entries that
// can be stored.
func (c *MessageCache) SetMaxCacheSize(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.cacheSizeSet = n
c.pruneLocked()
}
// Flush clears the cache.
func (c *MessageCache) Flush() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache.Clear()
}
// pruneLocked prunes down the cache size to the configured (or
// default) max size.
func (c *MessageCache) pruneLocked() {
max := c.cacheSizeSet
if max == 0 {
max = 500
}
for c.cache.Len() > max {
c.cache.RemoveOldest()
}
}
// msgQ is the MessageCache cache key.
//
// It's basically a golang.org/x/net/dns/dnsmessage#Question but the
// Class is omitted (we only cache ClassINET) and we store a Go string
// instead of a 256 byte dnsmessage.Name array.
type msgQ struct {
Name string
Type dnsmessage.Type // A, AAAA, MX, etc
}
// A *msgCacheValue is the cached value for a msgQ (question) key.
//
// Despite using pointers for storage and methods, the value is
// immutable once placed in the cache.
type msgCacheValue struct {
Expires time.Time
// Answers are the minimum data to reconstruct a DNS response
// message. TTLs are added later when converting to a
// dnsmessage.Resource.
Answers []msgResource
}
type msgResource struct {
Name string
Type dnsmessage.Type // dnsmessage.UnknownResource.Type
Data []byte // dnsmessage.UnknownResource.Data
}
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
// when the request can not be satisified from cache.
var ErrCacheMiss = errors.New("cache miss")
var parserPool = &sync.Pool{
New: func() interface{} { return new(dnsmessage.Parser) },
}
// ReplyFromCache writes a DNS reply to w for the provided DNS query message,
// which must begin with the two ID bytes of a DNS message.
//
// If there's a cache miss, the message is invalid or unexpected,
// ErrCacheMiss is returned. On cache hit, either nil or an error from
// a w.Write call is returned.
func (c *MessageCache) ReplyFromCache(w io.Writer, dnsQueryMessage []byte) error {
cacheKey, txID, ok := getDNSQueryCacheKey(dnsQueryMessage)
if !ok {
return ErrCacheMiss
}
now := c.now()
c.mu.Lock()
cacheEntI, _ := c.cache.Get(cacheKey)
v, ok := cacheEntI.(*msgCacheValue)
if ok && now.After(v.Expires) {
c.cache.Remove(cacheKey)
ok = false
}
c.mu.Unlock()
if !ok {
return ErrCacheMiss
}
ttl := uint32(v.Expires.Sub(now).Seconds())
packedRes, err := packDNSResponse(cacheKey, txID, ttl, v.Answers)
if err != nil {
return ErrCacheMiss
}
_, err = w.Write(packedRes)
return err
}
var (
errNotCacheable = errors.New("question not cacheable")
)
// AddCacheEntry adds a cache entry to the cache.
// It returns an error if the entry could not be cached.
func (c *MessageCache) AddCacheEntry(qPacket, res []byte) error {
cacheKey, qID, ok := getDNSQueryCacheKey(qPacket)
if !ok {
return errNotCacheable
}
now := c.now()
v := &msgCacheValue{}
p := parserPool.Get().(*dnsmessage.Parser)
defer parserPool.Put(p)
resh, err := p.Start(res)
if err != nil {
return fmt.Errorf("reading header in response: %w", err)
}
if resh.ID != qID {
return fmt.Errorf("response ID doesn't match query ID")
}
q, err := p.Question()
if err != nil {
return fmt.Errorf("reading 1st question in response: %w", err)
}
if _, err := p.Question(); err != dnsmessage.ErrSectionDone {
if err == nil {
return errors.New("unexpected 2nd question in response")
}
return fmt.Errorf("after reading 1st question in response: %w", err)
}
if resName := asciiLowerName(q.Name).String(); resName != cacheKey.Name {
return fmt.Errorf("response question name %q != question name %q", resName, cacheKey.Name)
}
for {
rh, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return fmt.Errorf("reading answer: %w", err)
}
res, err := p.UnknownResource()
if err != nil {
return fmt.Errorf("reading resource: %w", err)
}
if rh.Class != dnsmessage.ClassINET {
continue
}
// Set the cache entry's expiration to the soonest
// we've seen. (They should all be the same, though)
expires := now.Add(time.Duration(rh.TTL) * time.Second)
if v.Expires.IsZero() || expires.Before(v.Expires) {
v.Expires = expires
}
v.Answers = append(v.Answers, msgResource{
Name: rh.Name.String(),
Type: rh.Type,
Data: res.Data, // doesn't alias; a copy from dnsmessage.unpackUnknownResource
})
}
c.addCacheValue(cacheKey, v)
return nil
}
func (c *MessageCache) addCacheValue(cacheKey msgQ, v *msgCacheValue) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache.Add(cacheKey, v)
c.pruneLocked()
}
func getDNSQueryCacheKey(msg []byte) (cacheKey msgQ, txID uint16, ok bool) {
p := parserPool.Get().(*dnsmessage.Parser)
defer parserPool.Put(p)
h, err := p.Start(msg)
const dnsHeaderSize = 12
if err != nil || h.OpCode != 0 || h.Response || h.Truncated ||
len(msg) < dnsHeaderSize { // p.Start checks this anyway, but to be explicit for slicing below
return cacheKey, 0, false
}
var (
numQ = binary.BigEndian.Uint16(msg[4:6])
numAns = binary.BigEndian.Uint16(msg[6:8])
numAuth = binary.BigEndian.Uint16(msg[8:10])
numAddn = binary.BigEndian.Uint16(msg[10:12])
)
_ = numAddn // ignore this for now; do client OSes send EDNS additional? assume so, ignore.
if !(numQ == 1 && numAns == 0 && numAuth == 0) {
// Something weird. We don't want to deal with it.
return cacheKey, 0, false
}
q, err := p.Question()
if err != nil {
// Already verified numQ == 1 so shouldn't happen, but:
return cacheKey, 0, false
}
if q.Class != dnsmessage.ClassINET {
// We only cache the Internet class.
return cacheKey, 0, false
}
return msgQ{Name: asciiLowerName(q.Name).String(), Type: q.Type}, h.ID, true
}
func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
nb := n.Data[:]
if int(n.Length) < len(n.Data) {
nb = nb[:n.Length]
}
for i, b := range nb {
if 'A' <= b && b <= 'Z' {
n.Data[i] += 0x20
}
}
return n
}
// packDNSResponse builds a DNS response for the given question and
// transaction ID. The response resource records will have have the
// same provided TTL.
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
var baseMem []byte // TODO: guess a max size based on looping over answers?
b := dnsmessage.NewBuilder(baseMem, dnsmessage.Header{
ID: txID,
Response: true,
OpCode: 0,
Authoritative: false,
Truncated: false,
RCode: dnsmessage.RCodeSuccess,
})
name, err := dnsmessage.NewName(q.Name)
if err != nil {
return nil, err
}
if err := b.StartQuestions(); err != nil {
return nil, err
}
if err := b.Question(dnsmessage.Question{
Name: name,
Type: q.Type,
Class: dnsmessage.ClassINET,
}); err != nil {
return nil, err
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
for _, r := range answers {
name, err := dnsmessage.NewName(r.Name)
if err != nil {
return nil, err
}
if err := b.UnknownResource(dnsmessage.ResourceHeader{
Name: name,
Type: r.Type,
Class: dnsmessage.ClassINET,
TTL: ttl,
}, dnsmessage.UnknownResource{
Type: r.Type,
Data: r.Data,
}); err != nil {
return nil, err
}
}
return b.Finish()
}

View File

@@ -1,292 +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 dnscache
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"runtime"
"testing"
"time"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/tstest"
)
func TestMessageCache(t *testing.T) {
clock := &tstest.Clock{
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
}
mc := &MessageCache{Clock: clock.Now}
mc.SetMaxCacheSize(2)
clock.Advance(time.Second)
var out bytes.Buffer
if err := mc.ReplyFromCache(&out, makeQ(1, "foo.com.")); err != ErrCacheMiss {
t.Fatalf("unexpected error: %v", err)
}
if err := mc.AddCacheEntry(
makeQ(2, "foo.com."),
makeRes(2, "FOO.COM.", ttlOpt(10),
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}},
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 2}})); err != nil {
t.Fatal(err)
}
// Expect cache hit, with 10 seconds remaining.
out.Reset()
if err := mc.ReplyFromCache(&out, makeQ(3, "foo.com.")); err != nil {
t.Fatalf("expected cache hit; got: %v", err)
}
if p := mustParseResponse(t, out.Bytes()); p.TxID != 3 {
t.Errorf("TxID = %v; want %v", p.TxID, 3)
} else if p.TTL != 10 {
t.Errorf("TTL = %v; want 10", p.TTL)
}
// One second elapses, expect a cache hit, with 9 seconds
// remaining.
clock.Advance(time.Second)
out.Reset()
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.")); err != nil {
t.Fatalf("expected cache hit; got: %v", err)
}
if p := mustParseResponse(t, out.Bytes()); p.TxID != 4 {
t.Errorf("TxID = %v; want %v", p.TxID, 4)
} else if p.TTL != 9 {
t.Errorf("TTL = %v; want 9", p.TTL)
}
// Expect cache miss on MX record.
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.TypeMX)); err != ErrCacheMiss {
t.Fatalf("expected cache miss on MX; got: %v", err)
}
// Expect cache miss on CHAOS class.
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.ClassCHAOS)); err != ErrCacheMiss {
t.Fatalf("expected cache miss on CHAOS; got: %v", err)
}
// Ten seconds elapses; expect a cache miss.
clock.Advance(10 * time.Second)
if err := mc.ReplyFromCache(&out, makeQ(5, "foo.com.")); err != ErrCacheMiss {
t.Fatalf("expected cache miss, got: %v", err)
}
}
type parsedMeta struct {
TxID uint16
TTL uint32
}
func mustParseResponse(t testing.TB, r []byte) (ret parsedMeta) {
t.Helper()
var p dnsmessage.Parser
h, err := p.Start(r)
if err != nil {
t.Fatal(err)
}
ret.TxID = h.ID
qq, err := p.AllQuestions()
if err != nil {
t.Fatalf("AllQuestions: %v", err)
}
if len(qq) != 1 {
t.Fatalf("num questions = %v; want 1", len(qq))
}
aa, err := p.AllAnswers()
if err != nil {
t.Fatalf("AllAnswers: %v", err)
}
for _, r := range aa {
if ret.TTL == 0 {
ret.TTL = r.Header.TTL
}
if ret.TTL != r.Header.TTL {
t.Fatal("mixed TTLs")
}
}
return ret
}
type responseOpt bool
type ttlOpt uint32
func makeQ(txID uint16, name string, opt ...interface{}) []byte {
opt = append(opt, responseOpt(false))
return makeDNSPkt(txID, name, opt...)
}
func makeRes(txID uint16, name string, opt ...interface{}) []byte {
opt = append(opt, responseOpt(true))
return makeDNSPkt(txID, name, opt...)
}
func makeDNSPkt(txID uint16, name string, opt ...interface{}) []byte {
typ := dnsmessage.TypeA
class := dnsmessage.ClassINET
var response bool
var answers []dnsmessage.ResourceBody
var ttl uint32 = 1 // one second by default
for _, o := range opt {
switch o := o.(type) {
case dnsmessage.Type:
typ = o
case dnsmessage.Class:
class = o
case responseOpt:
response = bool(o)
case dnsmessage.ResourceBody:
answers = append(answers, o)
case ttlOpt:
ttl = uint32(o)
default:
panic(fmt.Sprintf("unknown opt type %T", o))
}
}
qname := dnsmessage.MustNewName(name)
msg := dnsmessage.Message{
Header: dnsmessage.Header{ID: txID, Response: response},
Questions: []dnsmessage.Question{
{
Name: qname,
Type: typ,
Class: class,
},
},
}
for _, rb := range answers {
msg.Answers = append(msg.Answers, dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: qname,
Type: typ,
Class: class,
TTL: ttl,
},
Body: rb,
})
}
buf, err := msg.Pack()
if err != nil {
panic(err)
}
return buf
}
func TestASCIILowerName(t *testing.T) {
n := asciiLowerName(dnsmessage.MustNewName("Foo.COM."))
if got, want := n.String(), "foo.com."; got != want {
t.Errorf("got = %q; want %q", got, want)
}
}
func TestGetDNSQueryCacheKey(t *testing.T) {
tests := []struct {
name string
pkt []byte
want msgQ
txID uint16
anyTX bool
}{
{
name: "empty",
},
{
name: "a",
pkt: makeQ(123, "foo.com."),
want: msgQ{"foo.com.", dnsmessage.TypeA},
txID: 123,
},
{
name: "aaaa",
pkt: makeQ(6, "foo.com.", dnsmessage.TypeAAAA),
want: msgQ{"foo.com.", dnsmessage.TypeAAAA},
txID: 6,
},
{
name: "normalize_case",
pkt: makeQ(123, "FoO.CoM."),
want: msgQ{"foo.com.", dnsmessage.TypeA},
txID: 123,
},
{
name: "ignore_response",
pkt: makeRes(123, "foo.com."),
},
{
name: "ignore_question_with_answers",
pkt: makeQ(2, "foo.com.", &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}),
},
{
name: "whatever_go_generates", // in case Go's net package grows functionality we don't handle
pkt: getGoNetPacketDNSQuery("from-go.foo."),
want: msgQ{"from-go.foo.", dnsmessage.TypeA},
anyTX: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotTX, ok := getDNSQueryCacheKey(tt.pkt)
if !ok {
if tt.txID == 0 && got == (msgQ{}) {
return
}
t.Fatal("failed")
}
if got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
if gotTX != tt.txID && !tt.anyTX {
t.Errorf("got tx %v, want %v", gotTX, tt.txID)
}
})
}
}
func getGoNetPacketDNSQuery(name string) []byte {
if runtime.GOOS == "windows" {
// On Windows, Go's net.Resolver doesn't use the DNS client.
// See https://github.com/golang/go/issues/33097 which
// was approved but not yet implemented.
// For now just pretend it's implemented to make this test
// pass on Windows with complicated the caller.
return makeQ(123, name)
}
res := make(chan []byte, 1)
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return goResolverConn(res), nil
},
}
r.LookupIP(context.Background(), "ip4", name)
return <-res
}
type goResolverConn chan<- []byte
func (goResolverConn) Close() error { return nil }
func (goResolverConn) LocalAddr() net.Addr { return todoAddr{} }
func (goResolverConn) RemoteAddr() net.Addr { return todoAddr{} }
func (goResolverConn) SetDeadline(t time.Time) error { return nil }
func (goResolverConn) SetReadDeadline(t time.Time) error { return nil }
func (goResolverConn) SetWriteDeadline(t time.Time) error { return nil }
func (goResolverConn) Read([]byte) (int, error) { return 0, errors.New("boom") }
func (c goResolverConn) Write(p []byte) (int, error) {
select {
case c <- p[2:]: // skip 2 byte length for TCP mode DNS query
default:
}
return 0, errors.New("boom")
}
type todoAddr struct{}
func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string { return "unused-todoAddr" }

View File

@@ -76,7 +76,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
if debugPipe {
orig := b
defer func() {
log.Printf("Pipe(%q).Read(%q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
log.Printf("Pipe(%q).Read( %q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
}()
}
for n == 0 {

View File

@@ -60,6 +60,9 @@ func TestPipeTimeout(t *testing.T) {
t.Run("block-write", func(t *testing.T) {
p := NewPipe("p1", 1<<16)
p.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
if _, err := p.Write([]byte{'h'}); err != nil {
t.Fatal(err)
}
if err := p.Block(); err != nil {
t.Fatal(err)
}
@@ -72,6 +75,9 @@ func TestPipeTimeout(t *testing.T) {
p.Write([]byte{'h', 'i'})
p.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
b := make([]byte, 1)
if _, err := p.Read(b); err != nil {
t.Fatal(err)
}
if err := p.Block(); err != nil {
t.Fatal(err)
}

View File

@@ -39,16 +39,6 @@ type Header interface {
Marshal(buf []byte) error
}
// HeaderChecksummer is implemented by Header implementations that
// need to do a checksum over their paylods.
type HeaderChecksummer interface {
Header
// WriteCheck writes the correct checksum into buf, which should
// be be the already-marshalled header and payload.
WriteChecksum(buf []byte)
}
// Generate generates a new packet with the given Header and
// payload. This function allocates memory, see Header.Marshal for an
// allocation-free option.
@@ -59,9 +49,5 @@ func Generate(h Header, payload []byte) []byte {
copy(buf[hlen:], payload)
h.Marshal(buf)
if hc, ok := h.(HeaderChecksummer); ok {
hc.WriteChecksum(buf)
}
return buf
}

View File

@@ -4,12 +4,6 @@
package packet
import (
"encoding/binary"
"tailscale.com/types/ipproto"
)
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
// including the outer IP layer or the variable "response data"
// trailer.
@@ -48,120 +42,3 @@ type ICMP6Code uint8
const (
ICMP6NoCode ICMP6Code = 0
)
// ICMP6Header is an IPv4+ICMPv4 header.
type ICMP6Header struct {
IP6Header
Type ICMP6Type
Code ICMP6Code
}
// Len implements Header.
func (h ICMP6Header) Len() int {
return h.IP6Header.Len() + icmp6HeaderLength
}
// Marshal implements Header.
func (h ICMP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ipproto.ICMPv6
h.IP6Header.Marshal(buf)
const o = ip6HeaderLength // start offset of ICMPv6 header
buf[o+0] = uint8(h.Type)
buf[o+1] = uint8(h.Code)
buf[o+2] = 0 // checksum, to be filled in later
buf[o+3] = 0 // checksum, to be filled in later
return nil
}
// ToResponse implements Header. TODO: it doesn't implement it
// correctly, instead it statically generates an ICMP Echo Reply
// packet.
func (h *ICMP6Header) ToResponse() {
// TODO: this doesn't implement ToResponse correctly, as it
// assumes the ICMP request type.
h.Type = ICMP6EchoReply
h.Code = ICMP6NoCode
h.IP6Header.ToResponse()
}
// WriteChecksum implements HeaderChecksummer, writing just the checksum bytes
// into the otherwise fully marshaled ICMP6 packet p (which should include the
// IPv6 header, ICMPv6 header, and payload).
func (h ICMP6Header) WriteChecksum(p []byte) {
const payOff = ip6HeaderLength + icmp6HeaderLength
xsum := icmp6Checksum(p[ip6HeaderLength:payOff], h.Src.As16(), h.Dst.As16(), p[payOff:])
binary.BigEndian.PutUint16(p[ip6HeaderLength+2:], xsum)
}
// Adapted from gVisor:
// icmp6Checksum calculates the ICMP checksum over the provided ICMPv6
// header (without the IPv6 header), IPv6 src/dst addresses and the
// payload.
//
// The header's existing checksum must be zeroed.
func icmp6Checksum(header []byte, src, dst [16]byte, payload []byte) uint16 {
// Calculate the IPv6 pseudo-header upper-layer checksum.
xsum := checksumBytes(src[:], 0)
xsum = checksumBytes(dst[:], xsum)
var scratch [4]byte
binary.BigEndian.PutUint32(scratch[:], uint32(len(header)+len(payload)))
xsum = checksumBytes(scratch[:], xsum)
xsum = checksumBytes(append(scratch[:0], 0, 0, 0, uint8(ipproto.ICMPv6)), xsum)
xsum = checksumBytes(payload, xsum)
var hdrz [icmp6HeaderLength]byte
copy(hdrz[:], header)
// Zero out the header.
hdrz[2] = 0
hdrz[3] = 0
xsum = ^checksumBytes(hdrz[:], xsum)
return xsum
}
// checksumCombine combines the two uint16 to form their
// checksum. This is done by adding them and the carry.
//
// Note that checksum a must have been computed on an even number of
// bytes.
func checksumCombine(a, b uint16) uint16 {
v := uint32(a) + uint32(b)
return uint16(v + v>>16)
}
// checksumBytes calculates the checksum (as defined in RFC 1071) of
// the bytes in buf.
//
// The initial checksum must have been computed on an even number of bytes.
func checksumBytes(buf []byte, initial uint16) uint16 {
v := uint32(initial)
odd := len(buf)%2 == 1
if odd {
v += uint32(buf[0])
buf = buf[1:]
}
n := len(buf)
odd = n&1 != 0
if odd {
n--
v += uint32(buf[n]) << 8
}
for i := 0; i < n; i += 2 {
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
}
return checksumCombine(uint16(v), uint16(v>>16))
}

View File

@@ -1,80 +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 packet
import (
"testing"
"inet.af/netaddr"
"tailscale.com/types/ipproto"
)
func TestICMPv6PingResponse(t *testing.T) {
pingHdr := ICMP6Header{
IP6Header: IP6Header{
Src: netaddr.MustParseIP("1::1"),
Dst: netaddr.MustParseIP("2::2"),
IPProto: ipproto.ICMPv6,
},
Type: ICMP6EchoRequest,
Code: ICMP6NoCode,
}
// echoReqLen is 2 bytes identifier + 2 bytes seq number.
// https://datatracker.ietf.org/doc/html/rfc4443#section-4.1
// Packet.IsEchoRequest verifies that these 4 bytes are present.
const echoReqLen = 4
buf := make([]byte, pingHdr.Len()+echoReqLen)
if err := pingHdr.Marshal(buf); err != nil {
t.Fatal(err)
}
var p Parsed
p.Decode(buf)
if !p.IsEchoRequest() {
t.Fatalf("not an echo request, got: %+v", p)
}
pingHdr.ToResponse()
buf = make([]byte, pingHdr.Len()+echoReqLen)
if err := pingHdr.Marshal(buf); err != nil {
t.Fatal(err)
}
p.Decode(buf)
if p.IsEchoRequest() {
t.Fatalf("unexpectedly still an echo request: %+v", p)
}
if !p.IsEchoResponse() {
t.Fatalf("not an echo response: %+v", p)
}
}
func TestICMPv6Checksum(t *testing.T) {
const req = "\x60\x0f\x07\x00\x00\x10\x3a\x40\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
"\x00\x00\x00\x00\x00\x00\x20\x0e\x80\x00\x4a\x9a\x2e\xea\x00\x02" +
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
// The packet that we'd originally generated incorrectly, but with the checksum
// bytes fixed per WireShark's correct calculation:
const wantRes = "\x60\x00\xf8\xff\x00\x10\x3a\x40\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
"\x00\x00\x00\x00\x00\x00\x20\x0e\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x81\x00\x49\x9a\x2e\xea\x00\x02" +
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
var p Parsed
p.Decode([]byte(req))
if !p.IsEchoRequest() {
t.Fatalf("not an echo request, got: %+v", p)
}
h := p.ICMP6Header()
h.ToResponse()
pong := Generate(&h, p.Payload())
if string(pong) != wantRes {
t.Errorf("wrong packet\n\n got: %x\nwant: %x", pong, wantRes)
}
}

View File

@@ -57,7 +57,7 @@ func (h *IP6Header) ToResponse() {
// marshalPseudo serializes h into buf in the "pseudo-header" form
// required when calculating UDP checksums.
func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
func (h IP6Header) marshalPseudo(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
@@ -72,6 +72,6 @@ func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
buf[36] = 0
buf[37] = 0
buf[38] = 0
buf[39] = byte(proto) // NextProto
buf[39] = 17 // NextProto
return nil
}

View File

@@ -75,7 +75,7 @@ func (p *Parsed) String() string {
}
// Decode extracts data from the packet in b into q.
// It performs extremely simple packet decoding for basic IPv4 and IPv6 packet types.
// It performs extremely simple packet decoding for basic IPv4 packet types.
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
// and shouldn't need any memory allocation.
func (q *Parsed) Decode(b []byte) {
@@ -339,6 +339,9 @@ func (q *Parsed) IP6Header() IP6Header {
}
func (q *Parsed) ICMP4Header() ICMP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
return ICMP4Header{
IP4Header: q.IP4Header(),
Type: ICMP4Type(q.b[q.subofs+0]),
@@ -346,15 +349,10 @@ func (q *Parsed) ICMP4Header() ICMP4Header {
}
}
func (q *Parsed) ICMP6Header() ICMP6Header {
return ICMP6Header{
IP6Header: q.IP6Header(),
Type: ICMP6Type(q.b[q.subofs+0]),
Code: ICMP6Code(q.b[q.subofs+1]),
}
}
func (q *Parsed) UDP4Header() UDP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
return UDP4Header{
IP4Header: q.IP4Header(),
SrcPort: q.Src.Port(),
@@ -412,7 +410,7 @@ func (q *Parsed) IsEchoRequest() bool {
}
}
// IsEchoResponse reports whether q is an IPv4 ICMP Echo Response.
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
func (q *Parsed) IsEchoResponse() bool {
switch q.IPProto {
case ipproto.ICMPv4:

View File

@@ -40,7 +40,7 @@ func (h UDP6Header) Marshal(buf []byte) error {
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
// UDP checksum with IP pseudo header.
h.IP6Header.marshalPseudo(buf, ipproto.UDP)
h.IP6Header.marshalPseudo(buf)
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
h.IP6Header.Marshal(buf)

View File

@@ -63,18 +63,11 @@ type igdCounters struct {
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
d := &TestIGD{
logf: logf,
doPMP: t.PMP,
doPCP: t.PCP,
doUPnP: t.UPnP,
}
d.logf = func(msg string, args ...interface{}) {
// Don't log after the device has closed;
// stray trailing logging angers testing.T.Logf.
if d.closed.Get() {
return
}
logf(msg, args...)
}
var err error
if d.upnpConn, err = testListenUDP(); err != nil {
return nil, err

View File

@@ -19,10 +19,6 @@ import (
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
// https://tools.ietf.org/html/rfc6887
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
type pcpResultCode uint8
// PCP constants
const (
pcpVersion = 2
@@ -30,14 +26,8 @@ const (
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
pcpCodeOK pcpResultCode = 0
pcpCodeNotAuthorized pcpResultCode = 2
// From RFC 6887:
// ADDRESS_MISMATCH: The source IP address of the request packet does
// not match the contents of the PCP Client's IP Address field, due
// to an unexpected NAT on the path between the PCP client and the
// PCP-controlled NAT or firewall.
pcpCodeAddressMismatch pcpResultCode = 12
pcpCodeOK = 0
pcpCodeNotAuthorized = 2
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
@@ -150,7 +140,7 @@ func pcpAnnounceRequest(myIP netaddr.IP) []byte {
type pcpResponse struct {
OpCode uint8
ResultCode pcpResultCode
ResultCode uint8
Lifetime uint32
Epoch uint32
}
@@ -160,7 +150,7 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
return
}
res.OpCode = b[1]
res.ResultCode = pcpResultCode(b[3])
res.ResultCode = b[3]
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true

View File

@@ -1,37 +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.
// Code generated by "stringer -type=pcpResultCode -trimprefix=pcpCode"; DO NOT EDIT.
package portmapper
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pcpCodeOK-0]
_ = x[pcpCodeNotAuthorized-2]
_ = x[pcpCodeAddressMismatch-12]
}
const (
_pcpResultCode_name_0 = "OK"
_pcpResultCode_name_1 = "NotAuthorized"
_pcpResultCode_name_2 = "AddressMismatch"
)
func (i pcpResultCode) String() string {
switch {
case i == 0:
return _pcpResultCode_name_0
case i == 2:
return _pcpResultCode_name_1
case i == 12:
return _pcpResultCode_name_2
default:
return "pcpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View File

@@ -1,32 +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.
// Code generated by "stringer -type=pmpResultCode -trimprefix=pmpCode"; DO NOT EDIT.
package portmapper
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pmpCodeOK-0]
_ = x[pmpCodeUnsupportedVersion-1]
_ = x[pmpCodeNotAuthorized-2]
_ = x[pmpCodeNetworkFailure-3]
_ = x[pmpCodeOutOfResources-4]
_ = x[pmpCodeUnsupportedOpcode-5]
}
const _pmpResultCode_name = "OKUnsupportedVersionNotAuthorizedNetworkFailureOutOfResourcesUnsupportedOpcode"
var _pmpResultCode_index = [...]uint8{0, 2, 20, 33, 47, 61, 78}
func (i pmpResultCode) String() string {
if i >= pmpResultCode(len(_pmpResultCode_index)-1) {
return "pmpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _pmpResultCode_name[_pmpResultCode_index[i]:_pmpResultCode_index[i+1]]
}

View File

@@ -22,7 +22,6 @@ import (
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
)
// Debug knobs for "tailscaled debug --portmap".
@@ -564,8 +563,6 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
}
}
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pmpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pmpResultCode -trimprefix=pmpCode
type pmpResultCode uint16
// NAT-PMP constants.
@@ -688,13 +685,11 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
if c.sawPMPRecently() {
res.PMP = true
} else if !DisablePMP {
metricPMPSent.Add(1)
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else if !DisablePCP {
metricPCPSent.Add(1)
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
}
if c.sawUPnPRecently() {
@@ -739,7 +734,6 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
// See https://github.com/tailscale/tailscale/issues/3197 for
// an example of a device that strictly implements UPnP, and
// only responds to multicast queries.
metricUPnPSent.Add(1)
uc.WriteTo(uPnPPacket, upnpAddr)
uc.WriteTo(uPnPPacket, upnpMulticastAddr)
}
@@ -765,15 +759,11 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
port := uint16(addr.(*net.UDPAddr).Port)
switch port {
case c.upnpPort():
metricUPnPResponse.Add(1)
if ip == gw && mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
meta, err := parseUPnPDiscoResponse(buf[:n])
if err != nil {
metricUPnPParseErr.Add(1)
c.logf("unrecognized UPnP discovery response; ignoring: %v", err)
continue
c.logf("unrecognized UPnP discovery response; ignoring")
}
metricUPnPOK.Add(1)
c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
res.UPnP = true
c.mu.Lock()
@@ -781,12 +771,10 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
if c.uPnPMeta != meta {
c.logf("UPnP meta changed: %+v", meta)
c.uPnPMeta = meta
metricUPnPUpdatedMeta.Add(1)
}
c.mu.Unlock()
}
case c.pxpPort(): // same value for PMP and PCP
metricPXPResponse.Add(1)
if pres, ok := parsePCPResponse(buf[:n]); ok {
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
pcpHeard = true
@@ -797,35 +785,25 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
case pcpCodeOK:
c.logf("[v1] Got PCP response: epoch: %v", pres.Epoch)
res.PCP = true
metricPCPOK.Add(1)
continue
case pcpCodeNotAuthorized:
// A PCP service is running, but refuses to
// provide port mapping services.
res.PCP = false
metricPCPNotAuthorized.Add(1)
continue
case pcpCodeAddressMismatch:
// A PCP service is running, but it is behind a NAT, so it can't help us.
res.PCP = false
metricPCPAddressMismatch.Add(1)
continue
default:
// Fall through to unexpected log line.
}
}
metricPCPUnhandledResponseCode.Add(1)
c.logf("unexpected PCP probe response: %+v", pres)
}
if pres, ok := parsePMPResponse(buf[:n]); ok {
if pres.OpCode != pmpOpReply|pmpOpMapPublicAddr {
c.logf("unexpected PMP probe response opcode: %+v", pres)
metricPMPUnhandledOpcode.Add(1)
continue
}
switch pres.ResultCode {
case pmpCodeOK:
metricPMPOK.Add(1)
c.logf("[v1] Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
res.PMP = true
c.mu.Lock()
@@ -834,20 +812,11 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
c.pmpLastEpoch = pres.SecondsSinceEpoch
c.mu.Unlock()
continue
case pmpCodeNotAuthorized:
metricPMPNotAuthorized.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
case pmpCodeNetworkFailure:
metricPMPNetworkFailure.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
case pmpCodeOutOfResources:
metricPMPOutOfResources.Add(1)
case pmpCodeNotAuthorized, pmpCodeNetworkFailure, pmpCodeOutOfResources:
// Normal failures.
c.logf("PMP probe failed due result code: %+v", pres)
continue
}
metricPMPUnhandledResponseCode.Add(1)
c.logf("unexpected PMP probe response: %+v", pres)
}
}
@@ -866,74 +835,3 @@ var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
// PCP/PMP metrics
var (
// metricPXPResponse counts the number of times we received a PMP/PCP response.
metricPXPResponse = clientmetric.NewCounter("portmap_pxp_response")
// metricPCPSent counts the number of times we sent a PCP request.
metricPCPSent = clientmetric.NewCounter("portmap_pcp_sent")
// metricPCPOK counts the number of times
// we received a successful PCP response.
metricPCPOK = clientmetric.NewCounter("portmap_pcp_ok")
// metricPCPAddressMismatch counts the number of times
// we received a PCP address mismatch result code.
metricPCPAddressMismatch = clientmetric.NewCounter("portmap_pcp_address_mismatch")
// metricPCPNotAuthorized counts the number of times
// we received a PCP not authorized result code.
metricPCPNotAuthorized = clientmetric.NewCounter("portmap_pcp_not_authorized")
// metricPCPUnhandledResponseCode counts the number of times
// we received an (as yet) unhandled PCP result code.
metricPCPUnhandledResponseCode = clientmetric.NewCounter("portmap_pcp_unhandled_response_code")
// metricPMPSent counts the number of times we sent a PMP request.
metricPMPSent = clientmetric.NewCounter("portmap_pmp_sent")
// metricPMPOK counts the number of times
// we received a succesful PMP response.
metricPMPOK = clientmetric.NewCounter("portmap_pmp_ok")
// metricPMPUnhandledOpcode counts the number of times
// we received an unhandled PMP opcode.
metricPMPUnhandledOpcode = clientmetric.NewCounter("portmap_pmp_unhandled_opcode")
// metricPMPUnhandledResponseCode counts the number of times
// we received an unhandled PMP result code.
metricPMPUnhandledResponseCode = clientmetric.NewCounter("portmap_pmp_unhandled_response_code")
// metricPMPOutOfResources counts the number of times
// we received a PCP out of resources result code.
metricPMPOutOfResources = clientmetric.NewCounter("portmap_pmp_out_of_resources")
// metricPMPNetworkFailure counts the number of times
// we received a PCP network failure result code.
metricPMPNetworkFailure = clientmetric.NewCounter("portmap_pmp_network_failure")
// metricPMPNotAuthorized counts the number of times
// we received a PCP not authorized result code.
metricPMPNotAuthorized = clientmetric.NewCounter("portmap_pmp_not_authorized")
)
// UPnP metrics
var (
// metricUPnPSent counts the number of times we sent a UPnP request.
metricUPnPSent = clientmetric.NewCounter("portmap_upnp_sent")
// metricUPnPResponse counts the number of times we received a UPnP response.
metricUPnPResponse = clientmetric.NewCounter("portmap_upnp_response")
// metricUPnPParseErr counts the number of times we failed to parse a UPnP response.
metricUPnPParseErr = clientmetric.NewCounter("portmap_upnp_parse_err")
// metricUPnPOK counts the number of times we received a usable UPnP response.
metricUPnPOK = clientmetric.NewCounter("portmap_upnp_ok")
// metricUPnPUpdatedMeta counts the number of times
// we received a UPnP response with a new meta.
metricUPnPUpdatedMeta = clientmetric.NewCounter("portmap_upnp_updated_meta")
)

View File

@@ -1,145 +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 proxymux splits a net.Listener in two, routing SOCKS5
// connections to one and HTTP requests to the other.
//
// It allows for hosting both a SOCKS5 proxy and an HTTP proxy on the
// same listener.
package proxymux
import (
"io"
"net"
"sync"
"time"
)
// SplitSOCKSAndHTTP accepts connections on ln and passes connections
// through to either socksListener or httpListener, depending the
// first byte sent by the client.
func SplitSOCKSAndHTTP(ln net.Listener) (socksListener, httpListener net.Listener) {
sl := &listener{
addr: ln.Addr(),
c: make(chan net.Conn),
closed: make(chan struct{}),
}
hl := &listener{
addr: ln.Addr(),
c: make(chan net.Conn),
closed: make(chan struct{}),
}
go splitSOCKSAndHTTPListener(ln, sl, hl)
return sl, hl
}
func splitSOCKSAndHTTPListener(ln net.Listener, sl, hl *listener) {
for {
conn, err := ln.Accept()
if err != nil {
sl.Close()
hl.Close()
return
}
go routeConn(conn, sl, hl)
}
}
func routeConn(c net.Conn, socksListener, httpListener *listener) {
if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil {
c.Close()
return
}
var b [1]byte
if _, err := io.ReadFull(c, b[:]); err != nil {
c.Close()
return
}
if err := c.SetReadDeadline(time.Time{}); err != nil {
c.Close()
return
}
conn := &connWithOneByte{
Conn: c,
b: b[0],
}
// First byte of a SOCKS5 session is a version byte set to 5.
var ln *listener
if b[0] == 5 {
ln = socksListener
} else {
ln = httpListener
}
select {
case ln.c <- conn:
case <-ln.closed:
c.Close()
}
}
type listener struct {
addr net.Addr
c chan net.Conn
mu sync.Mutex // serializes close() on closed. It's okay to receive on closed without locking.
closed chan struct{}
}
func (ln *listener) Accept() (net.Conn, error) {
// Once closed, reliably stay closed, don't race with attempts at
// further connections.
select {
case <-ln.closed:
return nil, net.ErrClosed
default:
}
select {
case ret := <-ln.c:
return ret, nil
case <-ln.closed:
return nil, net.ErrClosed
}
}
func (ln *listener) Close() error {
ln.mu.Lock()
defer ln.mu.Unlock()
select {
case <-ln.closed:
// Already closed
default:
close(ln.closed)
}
return nil
}
func (ln *listener) Addr() net.Addr {
return ln.addr
}
// connWithOneByte is a net.Conn that returns b for the first read
// request, then forwards everything else to Conn.
type connWithOneByte struct {
net.Conn
b byte
bRead bool
}
func (c *connWithOneByte) Read(bs []byte) (int, error) {
if c.bRead {
return c.Conn.Read(bs)
}
if len(bs) == 0 {
return 0, nil
}
c.bRead = true
bs[0] = c.b
return 1, nil
}

View File

@@ -1,172 +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 proxymux
import (
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"testing"
"tailscale.com/net/socks5"
)
func TestSplitSOCKSAndHTTP(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.checkURL(s.httpClient, false)
s.checkURL(s.socksClient, false)
}
func TestSplitSOCKSAndHTTPCloseSocks(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.socksListener.Close()
s.checkURL(s.httpClient, false)
s.checkURL(s.socksClient, true)
}
func TestSplitSOCKSAndHTTPCloseHTTP(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.httpListener.Close()
s.checkURL(s.httpClient, true)
s.checkURL(s.socksClient, false)
}
func TestSplitSOCKSAndHTTPCloseBoth(t *testing.T) {
s := mkWorld(t)
defer s.Close()
s.httpListener.Close()
s.socksListener.Close()
s.checkURL(s.httpClient, true)
s.checkURL(s.socksClient, true)
}
type world struct {
t *testing.T
// targetListener/target is the HTTP server the client wants to
// reach. It unconditionally responds with HTTP 418 "I'm a
// teapot".
targetListener net.Listener
target http.Server
targetURL string
// httpListener/httpProxy is an HTTP proxy that can proxy to
// target.
httpListener net.Listener
httpProxy http.Server
// socksListener/socksProxy is a SOCKS5 proxy that can dial
// targetListener.
socksListener net.Listener
socksProxy *socks5.Server
// jointListener is the mux that serves both HTTP and SOCKS5
// proxying.
jointListener net.Listener
// httpClient and socksClient are HTTP clients configured to proxy
// through httpProxy and socksProxy respectively.
httpClient *http.Client
socksClient *http.Client
}
func (s *world) checkURL(c *http.Client, wantErr bool) {
s.t.Helper()
resp, err := c.Get(s.targetURL)
if wantErr {
if err == nil {
s.t.Errorf("HTTP request succeeded unexpectedly: got HTTP code %d, wanted failure", resp.StatusCode)
}
} else if err != nil {
s.t.Errorf("HTTP request failed: %v", err)
} else if c := resp.StatusCode; c != http.StatusTeapot {
s.t.Errorf("unexpected status code: got %d, want %d", c, http.StatusTeapot)
}
}
func (s *world) Close() {
s.jointListener.Close()
s.socksListener.Close()
s.httpProxy.Close()
s.httpListener.Close()
s.target.Close()
s.targetListener.Close()
}
func mkWorld(t *testing.T) (ret *world) {
t.Helper()
ret = &world{
t: t,
}
var err error
ret.targetListener, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
ret.target = http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
}),
}
go ret.target.Serve(ret.targetListener)
ret.targetURL = fmt.Sprintf("http://%s/", ret.targetListener.Addr().String())
ret.jointListener, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
ret.socksListener, ret.httpListener = SplitSOCKSAndHTTP(ret.jointListener)
httpProxy := http.Server{
Handler: httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: ret.targetListener.Addr().String(),
Path: "/",
}),
}
go httpProxy.Serve(ret.httpListener)
socksProxy := socks5.Server{}
go socksProxy.Serve(ret.socksListener)
ret.httpClient = &http.Client{
Transport: &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) {
return &url.URL{
Scheme: "http",
Host: ret.jointListener.Addr().String(),
Path: "/",
}, nil
},
DisableKeepAlives: true, // one connection per request
},
}
ret.socksClient = &http.Client{
Transport: &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) {
return &url.URL{
Scheme: "socks5",
Host: ret.jointListener.Addr().String(),
Path: "/",
}, nil
},
DisableKeepAlives: true, // one connection per request
},
}
return ret
}

View File

@@ -0,0 +1,79 @@
// 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 tssocks is the glue between Tailscale and the net/socks5 package.
package tssocks
import (
"context"
"net"
"sync"
"inet.af/netaddr"
"tailscale.com/net/socks5"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
)
// NewServer returns a new SOCKS5 server configured to dial out to
// Tailscale addresses.
//
// The returned server is not yet listening. The caller must call
// Serve with a listener.
//
// If ns is non-nil, it is used for dialing when needed.
func NewServer(logf logger.Logf, e wgengine.Engine, ns *netstack.Impl) *socks5.Server {
d := &dialer{ns: ns}
e.AddNetworkMapCallback(d.onNewNetmap)
return &socks5.Server{
Logf: logf,
Dialer: d.DialContext,
}
}
// dialer is the Tailscale SOCKS5 dialer.
type dialer struct {
ns *netstack.Impl
mu sync.Mutex
dns netstack.DNSMap
}
func (d *dialer) onNewNetmap(nm *netmap.NetworkMap) {
d.mu.Lock()
defer d.mu.Unlock()
d.dns = netstack.DNSMapFromNetworkMap(nm)
}
func (d *dialer) resolve(ctx context.Context, addr string) (netaddr.IPPort, error) {
d.mu.Lock()
dns := d.dns
d.mu.Unlock()
return dns.Resolve(ctx, addr)
}
func (d *dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.resolve(ctx, addr)
if err != nil {
return nil, err
}
if d.ns != nil && d.useNetstackForIP(ipp.IP()) {
return d.ns.DialContextTCP(ctx, ipp.String())
}
var stdDialer net.Dialer
return stdDialer.DialContext(ctx, network, ipp.String())
}
func (d *dialer) useNetstackForIP(ip netaddr.IP) bool {
if d.ns == nil {
return false
}
// TODO(bradfitz): this isn't exactly right.
// We should also support subnets when the
// prefs are configured as such.
return tsaddr.IsTailscaleIP(ip)
}

View File

@@ -1,115 +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 tsdial
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"inet.af/netaddr"
"tailscale.com/types/netmap"
"tailscale.com/util/dnsname"
)
// dnsMap maps MagicDNS names (both base + FQDN) to their first IP.
// It must not be mutated once created.
//
// Example keys are "foo.domain.tld.beta.tailscale.net" and "foo",
// both without trailing dots.
type dnsMap map[string]netaddr.IP
func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
if nm == nil {
return nil
}
ret := make(dnsMap)
suffix := nm.MagicDNSSuffix()
have4 := false
if nm.Name != "" && len(nm.Addresses) > 0 {
ip := nm.Addresses[0].IP()
ret[strings.TrimRight(nm.Name, ".")] = ip
if dnsname.HasSuffix(nm.Name, suffix) {
ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip
}
for _, a := range nm.Addresses {
if a.IP().Is4() {
have4 = true
}
}
}
for _, p := range nm.Peers {
if p.Name == "" {
continue
}
for _, a := range p.Addresses {
ip := a.IP()
if ip.Is4() && !have4 {
continue
}
ret[strings.TrimRight(p.Name, ".")] = ip
if dnsname.HasSuffix(p.Name, suffix) {
ret[dnsname.TrimSuffix(p.Name, suffix)] = ip
}
break
}
}
for _, rec := range nm.DNS.ExtraRecords {
if rec.Type != "" {
continue
}
ip, err := netaddr.ParseIP(rec.Value)
if err != nil {
continue
}
ret[strings.TrimRight(rec.Name, ".")] = ip
}
return ret
}
// errUnresolved is a sentinel error returned by dnsMap.resolveMemory.
var errUnresolved = errors.New("address well formed but not resolved")
func splitHostPort(addr string) (host string, port uint16, err error) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return "", 0, err
}
port16, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return "", 0, fmt.Errorf("invalid port in address %q", addr)
}
return host, uint16(port16), nil
}
// Resolve resolves addr into an IP:port using first the MagicDNS contents
// of m, else using the system resolver.
//
// The error is [exactly] errUnresolved if the addr is a name that isn't known
// in the map.
func (m dnsMap) resolveMemory(ctx context.Context, network, addr string) (_ netaddr.IPPort, err error) {
host, port, err := splitHostPort(addr)
if err != nil {
// addr malformed or invalid port.
return netaddr.IPPort{}, err
}
if ip, err := netaddr.ParseIP(host); err == nil {
// addr was literal ip:port.
return netaddr.IPPortFrom(ip, port), nil
}
// Host is not an IP, so assume it's a DNS name.
// Try MagicDNS first, otherwise a real DNS lookup.
ip := m[host]
if !ip.IsZero() {
return netaddr.IPPortFrom(ip, port), nil
}
return netaddr.IPPort{}, errUnresolved
}

View File

@@ -1,101 +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 tsdial
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"tailscale.com/net/dnscache"
)
// dohConn is a net.PacketConn suitable for returning from
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
// ExitDNS DoH proxy service.
type dohConn struct {
ctx context.Context
baseURL string
hc *http.Client // if nil, default is used
dnsCache *dnscache.MessageCache
rbuf bytes.Buffer
}
var (
_ net.Conn = (*dohConn)(nil)
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
)
func (*dohConn) Close() error { return nil }
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
func (*dohConn) SetDeadline(t time.Time) error { return nil }
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return c.Write(p)
}
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = c.Read(p)
return n, todoAddr{}, err
}
func (c *dohConn) Read(p []byte) (n int, err error) {
return c.rbuf.Read(p)
}
func (c *dohConn) Write(packet []byte) (n int, err error) {
if c.dnsCache != nil {
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
if err == nil {
// Cache hit.
// TODO(bradfitz): add clientmetric
return len(packet), nil
}
c.rbuf.Reset()
}
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
if err != nil {
return 0, err
}
const dohType = "application/dns-message"
req.Header.Set("Content-Type", dohType)
hc := c.hc
if hc == nil {
hc = http.DefaultClient
}
hres, err := hc.Do(req)
if err != nil {
return 0, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
return 0, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
}
_, err = io.Copy(&c.rbuf, hres.Body)
if err != nil {
return 0, err
}
if c.dnsCache != nil {
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
}
return len(packet), nil
}
type todoAddr struct{}
func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string { return "unused-todoAddr" }

View File

@@ -1,32 +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 tsdial
import (
"context"
"flag"
"net"
"testing"
"time"
)
var dohBase = flag.String("doh-base", "", "DoH base URL for manual DoH tests; e.g. \"http://100.68.82.120:47830/dns-query\"")
func TestDoHResolve(t *testing.T) {
if *dohBase == "" {
t.Skip("skipping manual test without --doh-base= set")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var r net.Resolver
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{ctx: ctx, baseURL: *dohBase}, nil
}
addrs, err := r.LookupIP(ctx, "ip4", "google.com.")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %q", addrs)
}

View File

@@ -1,43 +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.
// This file's built on iOS and on two of three macOS build variants:
// the two GUI variants that both use Extensions (Network Extension
// and System Extension). It's not used on tailscaled-on-macOS.
//go:build ts_macext && (darwin || ios)
// +build ts_macext
// +build darwin ios
package tsdial
import (
"errors"
"net"
"syscall"
"tailscale.com/net/netns"
)
func init() {
peerDialControlFunc = peerDialControlFuncNetworkExtension
}
func peerDialControlFuncNetworkExtension(d *Dialer) func(network, address string, c syscall.RawConn) error {
d.mu.Lock()
defer d.mu.Unlock()
index := -1
if x, ok := d.interfaceIndexLocked(d.tunName); ok {
index = x
}
var lc net.ListenConfig
netns.SetListenConfigInterfaceIndex(&lc, index)
return func(network, address string, c syscall.RawConn) error {
if index == -1 {
return errors.New("failed to find TUN interface to bind to")
}
return lc.Control(network, address, c)
}
}

View File

@@ -1,281 +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 tsdial provides a Dialer type that can dial out of tailscaled.
package tsdial
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"inet.af/netaddr"
"tailscale.com/net/dnscache"
"tailscale.com/net/netknob"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/monitor"
)
// Dialer dials out of tailscaled, while taking care of details while
// handling the dozens of edge cases depending on the server mode
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
// Extension, none), user-selected route acceptance prefs, etc.
type Dialer struct {
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if
// it's non-nil) should be used to dial the provided IP.
UseNetstackForIP func(netaddr.IP) bool
// NetstackDialTCP dials the provided IPPort using netstack.
// If nil, it's not used.
NetstackDialTCP func(context.Context, netaddr.IPPort) (net.Conn, error)
peerDialControlFuncAtomic atomic.Value // of func() func(network, address string, c syscall.RawConn) error
peerClientOnce sync.Once
peerClient *http.Client
peerDialerOnce sync.Once
peerDialer *net.Dialer
mu sync.Mutex
dns dnsMap
tunName string // tun device name
linkMon *monitor.Mon
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
}
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
// etc). This is needed on some platforms to set sockopts to bind
// to the same interface index.
func (d *Dialer) SetTUNName(name string) {
d.mu.Lock()
defer d.mu.Unlock()
d.tunName = name
}
// TUNName returns the name of the tun device in use, if any.
// Example format ("tailscale0", "utun6").
func (d *Dialer) TUNName() string {
d.mu.Lock()
defer d.mu.Unlock()
return d.tunName
}
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
// The doh URL should contain the scheme, authority, and path, but without
// a '?' and/or query parameters.
//
// For example, "http://100.68.82.120:47830/dns-query".
func (d *Dialer) SetExitDNSDoH(doh string) {
d.mu.Lock()
defer d.mu.Unlock()
if d.exitDNSDoHBase == doh {
return
}
d.exitDNSDoHBase = doh
if doh != "" && d.dnsCache == nil {
d.dnsCache = new(dnscache.MessageCache)
}
if d.dnsCache != nil {
d.dnsCache.Flush()
}
}
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
d.mu.Lock()
defer d.mu.Unlock()
d.linkMon = mon
}
func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
if d.linkMon == nil {
return 0, false
}
st := d.linkMon.InterfaceState()
iface, ok := st.Interface[ifName]
if !ok {
return 0, false
}
return iface.Index, true
}
// peerDialControlFunc is non-nil on platforms that require a way to
// bind to dial out to other peers.
var peerDialControlFunc func(*Dialer) func(network, address string, c syscall.RawConn) error
// PeerDialControlFunc returns a function
// that can assigned to net.Dialer.Control to set sockopts or whatnot
// to make a dial escape the current platform's network sandbox.
//
// On many platforms the returned func will be nil.
//
// Notably, this is non-nil on iOS and macOS when run as a Network or
// System Extension (the GUI variants).
func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
if peerDialControlFunc == nil {
return nil
}
return peerDialControlFunc(d)
}
// SetNetMap sets the current network map and notably, the DNS names
// in its DNS configuration.
func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
m := dnsMapFromNetworkMap(nm)
d.mu.Lock()
defer d.mu.Unlock()
d.dns = m
}
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netaddr.IPPort, error) {
d.mu.Lock()
dns := d.dns
exitDNSDoH := d.exitDNSDoHBase
d.mu.Unlock()
// MagicDNS or otherwise baked in to the NetworkMap? Try that first.
ipp, err := dns.resolveMemory(ctx, network, addr)
if err != errUnresolved {
return ipp, err
}
// Otherwise, hit the network.
// TODO(bradfitz): wire up net/dnscache too.
host, port, err := splitHostPort(addr)
if err != nil {
// addr is malformed.
return netaddr.IPPort{}, err
}
var r net.Resolver
if exitDNSDoH != "" && runtime.GOOS != "windows" { // Windows: https://github.com/golang/go/issues/33097
r.PreferGo = true
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{
ctx: ctx,
baseURL: exitDNSDoH,
hc: d.PeerAPIHTTPClient(),
dnsCache: d.dnsCache,
}, nil
}
}
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
if err != nil {
return netaddr.IPPort{}, err
}
if len(ips) == 0 {
return netaddr.IPPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
}
ip, _ := netaddr.FromStdIP(ips[0])
return netaddr.IPPortFrom(ip, port), nil
}
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
// to the input value of "tcp", "tcp4", "udp6" etc network
// names.
func ipNetOfNetwork(n string) string {
if strings.HasSuffix(n, "4") {
return "ip4"
}
if strings.HasSuffix(n, "6") {
return "ip6"
}
return "ip"
}
// UserDial connects to the provided network address as if a user were initiating the dial.
// (e.g. from a SOCKS or HTTP outbound proxy)
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.userDialResolve(ctx, network, addr)
if err != nil {
return nil, err
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
if d.NetstackDialTCP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
return d.NetstackDialTCP(ctx, ipp)
}
// TODO(bradfitz): netns, etc
var stdDialer net.Dialer
return stdDialer.DialContext(ctx, network, ipp.String())
}
// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
//
// network must a "tcp" type, and addr must be an ip:port. Name resolution
// is not supported.
func (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, fmt.Errorf("peerAPI dial requires tcp; %q not supported", network)
}
ipp, err := netaddr.ParseIPPort(addr)
if err != nil {
return nil, fmt.Errorf("peerAPI dial requires ip:port, not name resolution: %w", err)
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
if d.NetstackDialTCP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
return d.NetstackDialTCP(ctx, ipp)
}
return d.getPeerDialer().DialContext(ctx, network, addr)
}
// getPeerDialer returns the *net.Dialer to use to dial peers to use
// peer API.
//
// This is not used in netstack mode.
//
// The primary function of this is to work on macOS & iOS's in the
// Network/System Extension so it can mark the dialer as staying
// withing the network namespace/sandbox.
func (d *Dialer) getPeerDialer() *net.Dialer {
d.peerDialerOnce.Do(func() {
d.peerDialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
Control: d.PeerDialControlFunc(),
}
})
return d.peerDialer
}
// PeerAPIHTTPClient returns an HTTP Client to call peers' peerapi
// endpoints. //
// The returned Client must not be mutated; it's owned by the Dialer
// and shared by callers.
func (d *Dialer) PeerAPIHTTPClient() *http.Client {
d.peerClientOnce.Do(func() {
t := http.DefaultTransport.(*http.Transport).Clone()
t.Dial = nil
t.DialContext = d.dialPeerAPI
d.peerClient = &http.Client{Transport: t}
})
return d.peerClient
}
// PeerAPITransport returns a Transport to call peers' peerapi
// endpoints.
//
// The returned value must not be mutated; it's owned by the Dialer
// and shared by callers.
func (d *Dialer) PeerAPITransport() *http.Transport {
return d.PeerAPIHTTPClient().Transport.(*http.Transport)
}

View File

@@ -119,13 +119,13 @@ func ensureStateDirPerms(dirPath string) error {
// We configure the DACL such that any files or directories created within
// dirPath will also inherit this DACL.
explicitAccess := []windows.EXPLICIT_ACCESS{
{
windows.EXPLICIT_ACCESS{
windows.GENERIC_ALL,
windows.SET_ACCESS,
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
userTrustee,
},
{
windows.EXPLICIT_ACCESS{
windows.GENERIC_ALL,
windows.SET_ACCESS,
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,

View File

@@ -15,13 +15,13 @@ func TestParsePort(t *testing.T) {
expect int
}
tests := []InOut{
{"1.2.3.4:5678", 5678},
{"0.0.0.0.999", 999},
{"1.2.3.4:*", 0},
{"5.5.5.5:0", 0},
{"[1::2]:5", 5},
{"[1::2].5", 5},
{"gibberish", -1},
InOut{"1.2.3.4:5678", 5678},
InOut{"0.0.0.0.999", 999},
InOut{"1.2.3.4:*", 0},
InOut{"5.5.5.5:0", 0},
InOut{"[1::2]:5", 5},
InOut{"[1::2].5", 5},
InOut{"gibberish", -1},
}
for _, io := range tests {

View File

@@ -48,9 +48,7 @@ func TestBasics(t *testing.T) {
}()
go func() {
s := DefaultConnectionStrategy(sock)
s.UsePort(port)
c, err := Connect(s)
c, err := Connect(sock, port)
if err != nil {
errs <- err
return

View File

@@ -11,8 +11,8 @@ import (
"syscall"
)
func connect(s *ConnectionStrategy) (net.Conn, error) {
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
func connect(path string, port uint16) (net.Conn, error) {
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return nil, err
}

View File

@@ -57,65 +57,10 @@ func tailscaledStillStarting() bool {
return tailscaledProcExists()
}
// A ConnectionStrategy is a plan for how to connect to tailscaled or equivalent (e.g. IPNExtension on macOS).
type ConnectionStrategy struct {
// For now, a ConnectionStrategy is just a unix socket path, a TCP port,
// and a flag indicating whether to try fallback connections options.
path string
port uint16
fallback bool
// Longer term, a ConnectionStrategy should be an ordered list of things to attempt,
// with just the information required to connection for each.
//
// We have at least these cases to consider (see issue 3530):
//
// tailscale sandbox | tailscaled sandbox | OS | connection
// ------------------|--------------------|---------|-----------
// no | no | unix | unix socket
// no | no | Windows | TCP/port
// no | no | wasm | memconn
// no | Network Extension | macOS | TCP/port/token, port/token from lsof
// no | System Extension | macOS | TCP/port/token, port/token from lsof
// yes | Network Extension | macOS | TCP/port/token, port/token from readdir
// yes | System Extension | macOS | TCP/port/token, port/token from readdir
//
// Note e.g. that port is only relevant as an input to Connect on Windows,
// that path is not relevant to Windows, and that neither matters to wasm.
}
// DefaultConnectionStrategy returns a default connection strategy.
// The default strategy is to attempt to connect in as many ways as possible.
// It uses path as the unix socket path, when applicable,
// and defaults to WindowsLocalPort for the TCP port when applicable.
// It falls back to auto-discovery across sandbox boundaries on macOS.
// TODO: maybe take no arguments, since path is irrelevant on Windows? Discussion in PR 3499.
func DefaultConnectionStrategy(path string) *ConnectionStrategy {
return &ConnectionStrategy{path: path, port: WindowsLocalPort, fallback: true}
}
// UsePort modifies s to use port for the TCP port when applicable.
// UsePort is only applicable on Windows, and only then
// when not using the default for Windows.
func (s *ConnectionStrategy) UsePort(port uint16) {
s.port = port
}
// UseFallback modifies s to set whether it should fall back
// to connecting to the macOS GUI's tailscaled
// if the Unix socket path wasn't reachable.
func (s *ConnectionStrategy) UseFallback(b bool) {
s.fallback = b
}
// ExactPath returns a connection strategy that only attempts to connect via path.
func ExactPath(path string) *ConnectionStrategy {
return &ConnectionStrategy{path: path, fallback: false}
}
// Connect connects to tailscaled using s
func Connect(s *ConnectionStrategy) (net.Conn, error) {
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
func Connect(path string, port uint16) (net.Conn, error) {
for {
c, err := connect(s)
c, err := connect(path, port)
if err != nil && tailscaledStillStarting() {
time.Sleep(250 * time.Millisecond)
continue

View File

@@ -17,6 +17,6 @@ func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error)
return ln, 1, err
}
func connect(_ *ConnectionStrategy) (net.Conn, error) {
func connect(path string, port uint16) (net.Conn, error) {
return memconn.Dial("memu", memName)
}

View File

@@ -32,5 +32,6 @@ func init() {
}
}
return false
}
}

View File

@@ -23,19 +23,19 @@ import (
)
// TODO(apenwarr): handle magic cookie auth
func connect(s *ConnectionStrategy) (net.Conn, error) {
func connect(path string, port uint16) (net.Conn, error) {
if runtime.GOOS == "js" {
return nil, errors.New("safesocket.Connect not yet implemented on js/wasm")
}
if runtime.GOOS == "darwin" && s.fallback && s.path == "" && s.port == 0 {
if runtime.GOOS == "darwin" && path == "" && port == 0 {
return connectMacOSAppSandbox()
}
pipe, err := net.Dial("unix", s.path)
pipe, err := net.Dial("unix", path)
if err != nil {
if runtime.GOOS == "darwin" && s.fallback {
if runtime.GOOS == "darwin" {
extConn, extErr := connectMacOSAppSandbox()
if extErr != nil {
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", s.path, err, extErr)
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", path, err, extErr)
}
return extConn, nil
}

View File

@@ -32,7 +32,7 @@ main() {
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
. /etc/os-release
case "$ID" in
ubuntu|pop|neon|zorin|elementary|linuxmint)
ubuntu|pop|neon)
OS="ubuntu"
VERSION="$VERSION_CODENAME"
PACKAGETYPE="apt"
@@ -68,23 +68,6 @@ main() {
APT_KEY_TYPE="keyring"
fi
;;
kali)
OS="debian"
PACKAGETYPE="apt"
YEAR="$(echo "$VERSION_ID" | cut -f1 -d.)"
APT_SYSTEMCTL_START=true
# Third-party keyrings became the preferred method of
# installation in Debian 11 (Bullseye), which Kali switched
# to in roughly 2021.x releases
if [ "$YEAR" -lt 2021 ]; then
# Kali VERSION_ID is "kali-rolling", which isn't distinguishing
VERSION="buster"
APT_KEY_TYPE="legacy"
else
VERSION="bullseye"
APT_KEY_TYPE="keyring"
fi
;;
centos)
OS="$ID"
VERSION="$VERSION_ID"
@@ -111,11 +94,6 @@ main() {
VERSION=""
PACKAGETYPE="dnf"
;;
rocky)
OS="fedora"
VERSION=""
PACKAGETYPE="dnf"
;;
amzn)
OS="amazon-linux"
VERSION="$VERSION_ID"
@@ -408,10 +386,6 @@ main() {
esac
$SUDO apt-get update
$SUDO apt-get install tailscale
if [ "$APT_SYSTEMCTL_START" = "true" ]; then
$SUDO systemctl enable --now tailscaled
$SUDO systemctl start tailscaled
fi
set +x
;;
yum)
@@ -425,7 +399,7 @@ main() {
dnf)
set -x
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
$SUDO dnf install -y tailscale
$SUDO dnf install tailscale
$SUDO systemctl enable --now tailscaled
set +x
;;

View File

@@ -379,49 +379,18 @@ func (h *Hostinfo) CheckRequestTags() error {
return nil
}
// ServiceProto is a service type. It's usually
// TCP ("tcp") or UDP ("udp"), but it can also have
// meta service values as defined in Service.Proto.
type ServiceProto string
const (
TCP = ServiceProto("tcp")
UDP = ServiceProto("udp")
PeerAPI4 = ServiceProto("peerapi4")
PeerAPI6 = ServiceProto("peerapi6")
PeerAPIDNS = ServiceProto("peerapi-dns-proxy")
TCP = ServiceProto("tcp")
UDP = ServiceProto("udp")
)
// Service represents a service running on a node.
type Service struct {
_ structs.Incomparable
// Proto is the type of service. It's usually the constant TCP
// or UDP ("tcp" or "udp"), but it can also be one of the
// following meta service values:
//
// * "peerapi4": peerapi is available on IPv4; Port is the
// port number that the peerapi is running on the
// node's Tailscale IPv4 address.
// * "peerapi6": peerapi is available on IPv6; Port is the
// port number that the peerapi is running on the
// node's Tailscale IPv6 address.
// * "peerapi-dns": the local peerapi service supports
// being a DNS proxy (when the node is an exit
// node). For this service, the Port number is really
// the version number of the service.
Proto ServiceProto
// Port is the port number.
//
// For Proto "peerapi-dns", it's the version number of the DNS proxy,
// currently 1.
Port uint16
// Description is the textual description of the service,
// usually the process name that's running.
Description string `json:",omitempty"`
_ structs.Incomparable
Proto ServiceProto // TCP or UDP
Port uint16 // port number service is listening on
Description string `json:",omitempty"` // text description of service
// TODO(apenwarr): allow advertising services on subnet IPs?
// TODO(apenwarr): add "tags" here for each service?
}
@@ -938,21 +907,6 @@ type DNSConfig struct {
// ExtraRecords contains extra DNS records to add to the
// MagicDNS config.
ExtraRecords []DNSRecord `json:",omitempty"`
// ExitNodeFilteredSuffixes are the the DNS suffixes that the
// node, when being an exit node DNS proxy, should not answer.
//
// The entries do not contain trailing periods and are always
// all lowercase.
//
// If an entry starts with a period, it's a suffix match (but
// suffix ".a.b" doesn't match "a.b"; a prefix is required).
//
// If an entry does not start with a period, it's an exact
// match.
//
// Matches are case insensitive.
ExitNodeFilteredSet []string
}
// DNSRecord is an extra DNS record to add to MagicDNS.

View File

@@ -208,22 +208,20 @@ func (src *DNSConfig) Clone() *DNSConfig {
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
dst.CertDomains = append(src.CertDomains[:0:0], src.CertDomains...)
dst.ExtraRecords = append(src.ExtraRecords[:0:0], src.ExtraRecords...)
dst.ExitNodeFilteredSet = append(src.ExitNodeFilteredSet[:0:0], src.ExitNodeFilteredSet...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
Resolvers []dnstype.Resolver
Routes map[string][]dnstype.Resolver
FallbackResolvers []dnstype.Resolver
Domains []string
Proxied bool
Nameservers []netaddr.IP
PerDomain bool
CertDomains []string
ExtraRecords []DNSRecord
ExitNodeFilteredSet []string
Resolvers []dnstype.Resolver
Routes map[string][]dnstype.Resolver
FallbackResolvers []dnstype.Resolver
Domains []string
Proxied bool
Nameservers []netaddr.IP
PerDomain bool
CertDomains []string
ExtraRecords []DNSRecord
}{})
// Clone makes a deep copy of RegisterResponse.

View File

@@ -8,7 +8,6 @@
package tsnet
import (
"context"
"errors"
"fmt"
"log"
@@ -21,14 +20,12 @@ import (
"sync"
"time"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/net/nettest"
"tailscale.com/net/tsdial"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
@@ -117,11 +114,9 @@ func (s *Server) start() error {
return err
}
dialer := new(tsdial.Dialer) // mutated below (before used)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
ListenPort: 0,
LinkMonitor: linkMon,
Dialer: dialer,
})
if err != nil {
return err
@@ -132,7 +127,7 @@ func (s *Server) start() error {
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer)
ns, err := netstack.Create(logf, tunDev, eng, magicConn)
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}
@@ -141,13 +136,6 @@ func (s *Server) start() error {
if err := ns.Start(); err != nil {
return fmt.Errorf("failed to start netstack: %w", err)
}
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := eng.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
statePath := filepath.Join(s.dir, "tailscaled.state")
store, err := ipn.NewFileStore(statePath)
@@ -156,7 +144,7 @@ func (s *Server) start() error {
}
logid := "tslib-TODO"
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}

View File

@@ -29,7 +29,6 @@ import (
"testing"
"time"
"github.com/klauspost/compress/zstd"
"go4.org/mem"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -43,68 +42,38 @@ import (
"tailscale.com/version"
)
// CleanupBinaries cleans up any resources created by calls to BinaryDir, TailscaleBinary, or TailscaledBinary.
// It should be called from TestMain after all tests have completed.
func CleanupBinaries() {
buildOnce.Do(func() {})
if binDir != "" {
os.RemoveAll(binDir)
// Binaries are the paths to a tailscaled and tailscale binary.
// These can be shared by multiple nodes.
type Binaries struct {
Dir string // temp dir for tailscale & tailscaled
Daemon string // tailscaled
CLI string // tailscale
}
// BuildTestBinaries builds tailscale and tailscaled, failing the test
// if they fail to compile.
func BuildTestBinaries(t testing.TB) *Binaries {
td := t.TempDir()
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
return &Binaries{
Dir: td,
Daemon: filepath.Join(td, "tailscaled"+exe()),
CLI: filepath.Join(td, "tailscale"+exe()),
}
}
// BinaryDir returns a directory containing test tailscale and tailscaled binaries.
// If any test calls BinaryDir, there must be a TestMain function that calls
// CleanupBinaries after all tests are complete.
func BinaryDir(tb testing.TB) string {
buildOnce.Do(func() {
binDir, buildErr = buildTestBinaries()
})
if buildErr != nil {
tb.Fatal(buildErr)
}
return binDir
}
// buildMu limits our use of "go build" to one at a time, so we don't
// fight Go's built-in caching trying to do the same build concurrently.
var buildMu sync.Mutex
// TailscaleBinary returns the path to the test tailscale binary.
// If any test calls TailscaleBinary, there must be a TestMain function that calls
// CleanupBinaries after all tests are complete.
func TailscaleBinary(tb testing.TB) string {
return filepath.Join(BinaryDir(tb), "tailscale"+exe())
}
func build(t testing.TB, outDir string, targets ...string) {
buildMu.Lock()
defer buildMu.Unlock()
// TailscaledBinary returns the path to the test tailscaled binary.
// If any test calls TailscaleBinary, there must be a TestMain function that calls
// CleanupBinaries after all tests are complete.
func TailscaledBinary(tb testing.TB) string {
return filepath.Join(BinaryDir(tb), "tailscaled"+exe())
}
t0 := time.Now()
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
var (
buildOnce sync.Once
buildErr error
binDir string
)
// buildTestBinaries builds tailscale and tailscaled.
// It returns the dir containing the binaries.
func buildTestBinaries() (string, error) {
bindir, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
err = build(bindir, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
if err != nil {
os.RemoveAll(bindir)
return "", err
}
return bindir, nil
}
func build(outDir string, targets ...string) error {
goBin, err := findGo()
if err != nil {
return err
}
goBin := findGo(t)
cmd := exec.Command(goBin, "install")
if version.IsRace() {
cmd.Args = append(cmd.Args, "-race")
@@ -113,7 +82,7 @@ func build(outDir string, targets ...string) error {
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
errOut, err := cmd.CombinedOutput()
if err == nil {
return nil
return
}
if strings.Contains(string(errOut), "when GOBIN is set") {
// Fallback slow path for cross-compiled binaries.
@@ -122,25 +91,25 @@ func build(outDir string, targets ...string) error {
cmd := exec.Command(goBin, "build", "-o", outFile, target)
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
if errOut, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
}
}
return nil
return
}
return fmt.Errorf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
}
func findGo() (string, error) {
func findGo(t testing.TB) string {
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
if fi, err := os.Stat(goBin); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("failed to find go at %v", goBin)
t.Fatalf("failed to find go at %v", goBin)
}
return "", fmt.Errorf("looking for go binary: %v", err)
t.Fatalf("looking for go binary: %v", err)
} else if !fi.Mode().IsRegular() {
return "", fmt.Errorf("%v is unexpected %v", goBin, fi.Mode())
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
}
return goBin, nil
return goBin
}
func exe() string {
@@ -270,15 +239,12 @@ func (lc *LogCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body io.Reader = r.Body
if r.Header.Get("Content-Encoding") == "zstd" {
var err error
var dec *zstd.Decoder
dec, err = smallzstd.NewDecoder(body)
body, err = smallzstd.NewDecoder(body)
if err != nil {
log.Printf("bad caught zstd: %v", err)
http.Error(w, err.Error(), 400)
return
}
defer dec.Close()
body = dec
}
bodyBytes, _ := ioutil.ReadAll(body)

View File

@@ -52,7 +52,6 @@ func TestMain(m *testing.M) {
os.Setenv("TS_DISABLE_UPNP", "true")
flag.Parse()
v := m.Run()
CleanupBinaries()
if v != 0 {
os.Exit(v)
}
@@ -63,12 +62,17 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
func TestOneNodeUpNoAuth(t *testing.T) {
func TestOneNodeUp_NoAuth(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp()
@@ -82,10 +86,15 @@ func TestOneNodeUpNoAuth(t *testing.T) {
func TestOneNodeExpiredKey(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp()
n1.AwaitRunning(t)
@@ -118,10 +127,14 @@ func TestOneNodeExpiredKey(t *testing.T) {
func TestCollectPanic(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n := newTestNode(t, env)
cmd := exec.Command(env.daemon, "--cleanup")
cmd := exec.Command(n.env.Binaries.Daemon, "--cleanup")
cmd.Env = append(os.Environ(),
"TS_PLEASE_PANIC=1",
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
@@ -130,7 +143,7 @@ func TestCollectPanic(t *testing.T) {
t.Logf("initial run: %s", got)
// Now we run it again, and on start, it will upload the logs to logcatcher.
cmd = exec.Command(env.daemon, "--cleanup")
cmd = exec.Command(n.env.Binaries.Daemon, "--cleanup")
cmd.Env = append(os.Environ(), "TS_LOG_TARGET="+n.env.LogCatcherServer.URL)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("cleanup failed: %v: %q", err, out)
@@ -149,10 +162,15 @@ func TestCollectPanic(t *testing.T) {
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
func TestStateSavedOnStart(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp()
@@ -183,14 +201,18 @@ func TestStateSavedOnStart(t *testing.T) {
d1.MustCleanShutdown(t)
}
func TestOneNodeUpAuth(t *testing.T) {
func TestOneNodeUp_Auth(t *testing.T) {
t.Parallel()
env := newTestEnv(t, configureControl(func(control *testcontrol.Server) {
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins, configureControl(func(control *testcontrol.Server) {
control.RequireAuth = true
}))
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitListening(t)
@@ -228,16 +250,21 @@ func TestOneNodeUpAuth(t *testing.T) {
func TestTwoNodes(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
// Create two nodes:
n1 := newTestNode(t, env)
n1SocksAddrCh := n1.socks5AddrChan()
d1 := n1.StartDaemon(t)
defer d1.Kill()
n2 := newTestNode(t, env)
n2SocksAddrCh := n2.socks5AddrChan()
d2 := n2.StartDaemon(t)
defer d2.Kill()
n1Socks := n1.AwaitSocksAddr(t, n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(t, n2SocksAddrCh)
@@ -274,9 +301,14 @@ func TestTwoNodes(t *testing.T) {
func TestNodeAddressIPFields(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitListening(t)
n1.MustUp()
@@ -300,9 +332,14 @@ func TestNodeAddressIPFields(t *testing.T) {
func TestAddPingRequest(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
n1.StartDaemon(t)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitListening(t)
n1.MustUp()
@@ -354,10 +391,15 @@ func TestAddPingRequest(t *testing.T) {
// be connected to control.
func TestNoControlConnWhenDown(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
defer d1.Kill()
n1.AwaitResponding(t)
// Come up the first time.
@@ -371,6 +413,7 @@ func TestNoControlConnWhenDown(t *testing.T) {
env.LogCatcher.Reset()
d2 := n1.StartDaemon(t)
defer d2.Kill()
n1.AwaitResponding(t)
st := n1.MustStatus(t)
@@ -395,11 +438,16 @@ func TestNoControlConnWhenDown(t *testing.T) {
// without the GUI to kick off a Start.
func TestOneNodeUpWindowsStyle(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bins := BuildTestBinaries(t)
env := newTestEnv(t, bins)
defer env.Close()
n1 := newTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS(t, "windows")
defer d1.Kill()
n1.AwaitResponding(t)
n1.MustUp("--unattended")
@@ -412,9 +460,8 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
// testEnv contains the test environment (set of servers) used by one
// or more nodes.
type testEnv struct {
t testing.TB
cli string
daemon string
t testing.TB
Binaries *Binaries
LogCatcher *LogCatcher
LogCatcherServer *httptest.Server
@@ -436,9 +483,11 @@ func (f configureControl) modifyTestEnv(te *testEnv) {
f(te.Control)
}
// newTestEnv starts a bunch of services and returns a new test environment.
// newTestEnv arranges for the environment's resources to be cleaned up on exit.
func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
// newTestEnv starts a bunch of services and returns a new test
// environment.
//
// Call Close to shut everything down.
func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
if runtime.GOOS == "windows" {
t.Skip("not tested/working on Windows yet")
}
@@ -451,8 +500,7 @@ func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
trafficTrap := new(trafficTrap)
e := &testEnv{
t: t,
cli: TailscaleBinary(t),
daemon: TailscaledBinary(t),
Binaries: bins,
LogCatcher: logc,
LogCatcherServer: httptest.NewServer(logc),
Control: control,
@@ -464,19 +512,21 @@ func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
o.modifyTestEnv(e)
}
control.HTTPTestServer.Start()
t.Cleanup(func() {
// Shut down e.
if err := e.TrafficTrap.Err(); err != nil {
e.t.Errorf("traffic trap: %v", err)
e.t.Logf("logs: %s", e.LogCatcher.logsString())
}
e.LogCatcherServer.Close()
e.TrafficTrapServer.Close()
e.ControlServer.Close()
})
return e
}
func (e *testEnv) Close() error {
if err := e.TrafficTrap.Err(); err != nil {
e.t.Errorf("traffic trap: %v", err)
e.t.Logf("logs: %s", e.LogCatcher.logsString())
}
e.LogCatcherServer.Close()
e.TrafficTrapServer.Close()
e.ControlServer.Close()
return nil
}
// testNode is a machine with a tailscale & tailscaled.
// Currently, the test is simplistic and user==node==machine.
// That may grow complexity later to test more.
@@ -631,6 +681,10 @@ type Daemon struct {
Process *os.Process
}
func (d *Daemon) Kill() {
d.Process.Kill()
}
func (d *Daemon) MustCleanShutdown(t testing.TB) {
d.Process.Signal(os.Interrupt)
ps, err := d.Process.Wait()
@@ -642,14 +696,14 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
}
}
// StartDaemon starts the node's tailscaled, failing if it fails to start.
// StartDaemon ensures that the process will exit when the test completes.
// StartDaemon starts the node's tailscaled, failing if it fails to
// start.
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
return n.StartDaemonAsIPNGOOS(t, runtime.GOOS)
}
func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
cmd := exec.Command(n.env.daemon,
cmd := exec.Command(n.env.Binaries.Daemon,
"--tun=userspace-networking",
"--state="+n.stateFile,
"--socket="+n.sockFile,
@@ -670,7 +724,6 @@ func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
if err := cmd.Start(); err != nil {
t.Fatalf("starting tailscaled: %v", err)
}
t.Cleanup(func() { cmd.Process.Kill() })
return &Daemon{
Process: cmd.Process,
}
@@ -683,11 +736,8 @@ func (n *testNode) MustUp(extraArgs ...string) {
"--login-server=" + n.env.ControlServer.URL,
}
args = append(args, extraArgs...)
cmd := n.Tailscale(args...)
t.Logf("Running %v ...", cmd)
cmd.Stdout = nil // in case --verbose-tailscale was set
cmd.Stderr = nil // in case --verbose-tailscale was set
if b, err := cmd.CombinedOutput(); err != nil {
t.Logf("Running %v ...", args)
if b, err := n.Tailscale(args...).CombinedOutput(); err != nil {
t.Fatalf("up: %v, %v", string(b), err)
}
}
@@ -703,10 +753,8 @@ func (n *testNode) MustDown() {
// AwaitListening waits for the tailscaled to be serving local clients
// over its localhost IPC mechanism. (Unix socket, etc)
func (n *testNode) AwaitListening(t testing.TB) {
s := safesocket.DefaultConnectionStrategy(n.sockFile)
s.UseFallback(false) // connect only to the tailscaled that we started
if err := tstest.WaitFor(20*time.Second, func() (err error) {
c, err := safesocket.Connect(s)
c, err := safesocket.Connect(n.sockFile, safesocket.WindowsLocalPort)
if err != nil {
return err
}
@@ -793,7 +841,7 @@ func (n *testNode) AwaitNeedsLogin(t testing.TB) {
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
// It does not start the process.
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile)
cmd := exec.Command(n.env.Binaries.CLI, "--socket="+n.sockFile)
cmd.Args = append(cmd.Args, arg...)
cmd.Dir = n.dir
cmd.Env = append(os.Environ(),

View File

@@ -22,9 +22,7 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -22,9 +22,7 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -22,9 +22,7 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -22,9 +22,7 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

View File

@@ -26,9 +26,7 @@ import (
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
_ "tailscale.com/net/proxymux"
_ "tailscale.com/net/socks5"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/socks5/tssocks"
_ "tailscale.com/net/tshttpproxy"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"

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