Compare commits
90 Commits
bradfitz/e
...
aaron/logl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96188ffd2f | ||
|
|
486059589b | ||
|
|
59f4f33f60 | ||
|
|
ac8e69b713 | ||
|
|
0f3b55c299 | ||
|
|
4691e012a9 | ||
|
|
e133bb570b | ||
|
|
adc97e9c4d | ||
|
|
d24a8f7b5a | ||
|
|
8dbda1a722 | ||
|
|
cced414c7d | ||
|
|
cab5c46481 | ||
|
|
63cd581c3f | ||
|
|
a5235e165c | ||
|
|
c8829b742b | ||
|
|
39ffa16853 | ||
|
|
b59e7669c1 | ||
|
|
21741e111b | ||
|
|
7b9c7bc42b | ||
|
|
affc4530a2 | ||
|
|
485bcdc951 | ||
|
|
878a20df29 | ||
|
|
a28d280b95 | ||
|
|
9f867ad2c5 | ||
|
|
c0701b130d | ||
|
|
656809e4ee | ||
|
|
e34ba3223c | ||
|
|
c18dc57861 | ||
|
|
ffb16cdffb | ||
|
|
d3d503d997 | ||
|
|
abc00e9c8d | ||
|
|
190b7a4cca | ||
|
|
0d8ef1ff35 | ||
|
|
329751c48e | ||
|
|
9ddef8cdbf | ||
|
|
9140f193bc | ||
|
|
05c1be3e47 | ||
|
|
e6e63c2305 | ||
|
|
c0984f88dc | ||
|
|
eeccbccd08 | ||
|
|
69de3bf7bf | ||
|
|
1813c2a162 | ||
|
|
0a9932f3b2 | ||
|
|
9c5c9d0a50 | ||
|
|
9f6249b26d | ||
|
|
de635ac0a8 | ||
|
|
003089820d | ||
|
|
03a323de4e | ||
|
|
a8f60cf6e8 | ||
|
|
f91481075d | ||
|
|
adc5997592 | ||
|
|
768baafcb5 | ||
|
|
43983a4a3b | ||
|
|
44d0c1ab06 | ||
|
|
8775c646be | ||
|
|
ad3d6e31f0 | ||
|
|
25eab78573 | ||
|
|
c7fb26acdb | ||
|
|
c37af58ea4 | ||
|
|
bf1d69f25b | ||
|
|
2075c39fd7 | ||
|
|
49a9e62d58 | ||
|
|
56c72d9cde | ||
|
|
d5405c66b7 | ||
|
|
3ae6f898cf | ||
|
|
16abd7e07c | ||
|
|
2a95ee4680 | ||
|
|
deb2f5e793 | ||
|
|
f93cf6fa03 | ||
|
|
b800663779 | ||
|
|
124363e0ca | ||
|
|
e16cb523aa | ||
|
|
a8cc519c70 | ||
|
|
fddf43f3d1 | ||
|
|
9787ec6f4a | ||
|
|
40f11c50a1 | ||
|
|
38d90fa330 | ||
|
|
999814e9e1 | ||
|
|
bb91cfeae7 | ||
|
|
3181bbb8e4 | ||
|
|
46a9782322 | ||
|
|
d89c61b812 | ||
|
|
341e1af873 | ||
|
|
b811a316bc | ||
|
|
6e584ffa33 | ||
|
|
a54d13294f | ||
|
|
135580a5a8 | ||
|
|
d9c21936c3 | ||
|
|
1e8b4e770a | ||
|
|
105c545366 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
go.mod filter=go-mod
|
||||
*.go diff=golang
|
||||
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -2,15 +2,20 @@
|
||||
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "go.mod:"
|
||||
## 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: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: ".github:"
|
||||
|
||||
2
.github/workflows/cifuzz.yml
vendored
2
.github/workflows/cifuzz.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
|
||||
8
Makefile
8
Makefile
@@ -30,3 +30,11 @@ 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
|
||||
|
||||
@@ -8,11 +8,12 @@ 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 primarily on Linux; it also works to varying degrees on
|
||||
FreeBSD, OpenBSD, Darwin, and Windows.
|
||||
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.)
|
||||
|
||||
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
|
||||
|
||||
@@ -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="${VERSION_SHORT},${VERSION_MINOR}" \
|
||||
--tags="v${VERSION_SHORT},v${VERSION_MINOR}" \
|
||||
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
|
||||
--push
|
||||
|
||||
@@ -38,6 +38,9 @@ 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
|
||||
@@ -47,7 +50,8 @@ 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)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// TODO: make this part of a safesocket.ConnectionStrategy
|
||||
if !TailscaledSocketSetExplicitly {
|
||||
// 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.
|
||||
@@ -56,7 +60,11 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, safesocket.WindowsLocalPort)
|
||||
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)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -90,6 +98,27 @@ 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
|
||||
}
|
||||
@@ -140,23 +169,11 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
res, err := doLocalRequestNiceError(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
|
||||
@@ -228,7 +245,7 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "")
|
||||
}
|
||||
|
||||
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
|
||||
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "?peers=false")
|
||||
}
|
||||
@@ -295,6 +312,30 @@ 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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -36,6 +37,7 @@ 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")
|
||||
@@ -250,24 +252,26 @@ 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)
|
||||
})
|
||||
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)
|
||||
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,
|
||||
}
|
||||
}
|
||||
}()
|
||||
err := port80srv.ListenAndServe()
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
log.Printf("derper: serving on %s", *addr)
|
||||
|
||||
@@ -164,6 +164,11 @@ 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) {
|
||||
@@ -191,7 +196,8 @@ var rootArgs struct {
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
|
||||
@@ -18,8 +18,10 @@ 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.
|
||||
@@ -57,6 +59,7 @@ 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
|
||||
}{
|
||||
@@ -313,6 +316,7 @@ 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,
|
||||
@@ -329,7 +333,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: "windows",
|
||||
goos: "openbsd",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
@@ -405,6 +409,21 @@ 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"},
|
||||
@@ -427,6 +446,38 @@ 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) {
|
||||
@@ -447,6 +498,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
@@ -495,6 +547,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
@@ -532,7 +585,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
@@ -806,3 +859,133 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
found := false
|
||||
var foundProc ps.Process
|
||||
for _, proc := range procs {
|
||||
base := filepath.Base(proc.Executable())
|
||||
if base == "tailscaled" {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "darwin" && base == "IPNExtension" {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if foundProc == nil {
|
||||
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). Got error: %w", origErr)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -30,6 +28,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -96,7 +95,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
|
||||
stableID, isOffline, err := getTargetStableID(ctx, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
@@ -154,32 +153,21 @@ func runCp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
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("sending to %v ...", dstURL)
|
||||
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
continue
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sent %q", name)
|
||||
}
|
||||
io.Copy(Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
|
||||
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
@@ -195,7 +183,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl
|
||||
continue
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return ft.PeerAPIURL, isOffline, nil
|
||||
return n.StableID, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", false, fileTargetErrorDetail(ctx, ip)
|
||||
|
||||
@@ -29,7 +29,22 @@ var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [--active] [--web] [--json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
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,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
@@ -145,11 +160,19 @@ 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")
|
||||
f("idle; exit node" + offline)
|
||||
} else if ps.ExitNodeOption {
|
||||
f("idle; offers exit node" + offline)
|
||||
} else if anyTraffic {
|
||||
f("idle")
|
||||
f("idle" + offline)
|
||||
} else if !ps.Online {
|
||||
f("offline")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
@@ -157,12 +180,17 @@ 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)
|
||||
|
||||
@@ -6,6 +6,8 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -28,6 +30,8 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -46,8 +50,10 @@ 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.
|
||||
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.)
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: runUp,
|
||||
@@ -60,20 +66,34 @@ 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", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
|
||||
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.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\")")
|
||||
@@ -121,6 +141,7 @@ type upArgsT struct {
|
||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||
hostname string
|
||||
opUser string
|
||||
json bool
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
@@ -138,6 +159,33 @@ 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...)
|
||||
}
|
||||
@@ -190,6 +238,65 @@ 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
|
||||
@@ -205,9 +312,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
exitNodeIP, err = exitNodeIPOfArg(upArgs.exitNodeIP, st)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
return nil, err
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
@@ -380,11 +487,12 @@ 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(prefs, st),
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
}
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
@@ -435,10 +543,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
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())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.Running, "")
|
||||
} else if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
@@ -451,15 +565,33 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
|
||||
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))
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -546,6 +678,16 @@ 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
|
||||
)
|
||||
@@ -588,7 +730,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":
|
||||
case "authkey", "force-reauth", "reset", "qr", "json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -622,6 +764,7 @@ type upCheckEnv struct {
|
||||
upArgs upArgsT
|
||||
backendState string
|
||||
curExitNodeIP netaddr.IP
|
||||
distro distro.Distro
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
@@ -672,6 +815,10 @@ 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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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+
|
||||
@@ -72,7 +73,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
|
||||
@@ -91,7 +92,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
|
||||
|
||||
@@ -25,7 +25,8 @@ 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/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
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/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+
|
||||
@@ -62,6 +63,7 @@ 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
|
||||
@@ -179,7 +181,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
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
W tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy+
|
||||
@@ -193,16 +195,17 @@ 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/ipn/localapi+
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
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/socks5 from tailscale.com/net/socks5/tssocks
|
||||
tailscale.com/net/socks5/tssocks 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/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+
|
||||
@@ -250,7 +253,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
|
||||
@@ -273,7 +276,9 @@ 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/hpack 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/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+
|
||||
@@ -364,7 +369,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+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
|
||||
@@ -28,13 +28,16 @@ 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/socks5/tssocks"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -175,8 +178,7 @@ func main() {
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,43 +300,55 @@ func run() error {
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
return fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
|
||||
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, e)
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
|
||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||
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", srv.Serve(socksListener))
|
||||
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -364,13 +378,11 @@ func run() error {
|
||||
|
||||
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
||||
if err != nil {
|
||||
logf("ipnserver.StateStore: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.StateStore: %w", err)
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
@@ -385,21 +397,20 @@ 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 {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.Run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (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, name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
|
||||
if err == nil {
|
||||
return e, useNetstack, nil
|
||||
}
|
||||
@@ -431,10 +442,11 @@ func shouldWrapNetstack() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
}
|
||||
|
||||
useNetstack = name == "userspace-networking"
|
||||
@@ -508,26 +520,54 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, 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)
|
||||
return netstack.Create(logf, tunDev, e, magicConn, dialer)
|
||||
}
|
||||
|
||||
func mustStartTCPListener(name, addr string) net.Listener {
|
||||
if addr == "" {
|
||||
return nil
|
||||
// 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)
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("%v listener: %v", name, err)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
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())
|
||||
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())
|
||||
}
|
||||
}
|
||||
return ln
|
||||
|
||||
return socksListener, httpListener
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ 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"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -78,7 +80,10 @@ 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.
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
// 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)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
@@ -114,6 +119,9 @@ 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)
|
||||
|
||||
@@ -177,6 +185,12 @@ 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 {
|
||||
@@ -197,17 +211,19 @@ 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,
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
ns, err := newNetstack(logf, eng)
|
||||
ns, err := newNetstack(logf, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
@@ -287,7 +303,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
|
||||
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
|
||||
@@ -339,11 +339,9 @@ func (c *Auto) authRoutine() {
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
if goal.url != "" {
|
||||
err = fmt.Errorf("[unexpected] server required a new URL?")
|
||||
report(err, "WaitLoginURL")
|
||||
}
|
||||
|
||||
// goal.url ought to be empty here.
|
||||
// However, not all control servers get this right,
|
||||
// and logging about it here just generates noise.
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: true,
|
||||
|
||||
@@ -139,6 +139,9 @@ func TestNoReuse(t *testing.T) {
|
||||
t.Fatalf("server wire traffic seen twice")
|
||||
}
|
||||
packets[serverWire] = true
|
||||
|
||||
server.Close()
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
245
go.mod
245
go.mod
@@ -7,208 +7,257 @@ 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 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/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/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-0.20210915003542-8b1f7f90f6b1
|
||||
github.com/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/google/go-cmp v0.5.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-20210621130208-1cac67f12b1e
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211026125128-ad197bcd36fd
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.13.6
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/netlink v1.4.2
|
||||
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.0
|
||||
github.com/peterbourgon/ff/v3 v3.1.2
|
||||
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-20201214215404-77d1e9757027
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
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-20200924210142-dde312d0d6a2
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
|
||||
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-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495
|
||||
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
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
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.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.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-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
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
|
||||
github.com/Djarvur/go-err113 v0.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // 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.4.16 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 // 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/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/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/bombsimon/wsl/v3 v3.1.0 // 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/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
||||
github.com/daixiang0/gci v0.2.7 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.3.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // 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/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/go-git/gcfg v1.5.0 // 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-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // 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.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.0.1 // 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-20191208150333-d5b6f63a941b // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // 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.33.0 // indirect
|
||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc // indirect
|
||||
github.com/golangci/golangci-lint v1.43.0 // 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/prealloc v0.0.0-20180630174525-215b22d4de21 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
|
||||
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
|
||||
github.com/goreleaser/chglog v0.1.2 // indirect
|
||||
github.com/goreleaser/fileglob v0.3.1 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.6.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.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/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 // indirect
|
||||
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a // indirect
|
||||
github.com/jgautheron/goconst v1.5.1 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // 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/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // 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/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/kunwardeep/paralleltest v1.0.2 // indirect
|
||||
github.com/kulti/thelper v0.4.0 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.3 // indirect
|
||||
github.com/kyoh86/exportloopref v0.1.8 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // 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/maratori/testpackage v1.0.1 // 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/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/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moricho/tparallel v0.2.1 // 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/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/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 // indirect
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // 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-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/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/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.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.9.3 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sivchari/tenv v1.4.7 // indirect
|
||||
github.com/sonatard/noctx v0.0.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.6.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/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.2.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect
|
||||
github.com/spf13/viper v1.9.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // 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/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/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/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.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // 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
|
||||
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.4.2 // indirect
|
||||
golang.org/x/mod v0.5.1 // 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
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
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
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
mvdan.cc/gofumpt v0.2.0 // 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-20200501210554-b37ab49443f7 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 // indirect
|
||||
)
|
||||
|
||||
@@ -232,32 +232,11 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
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"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 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_NOT_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
@@ -344,3 +323,48 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -36,6 +35,7 @@ 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,6 +88,7 @@ 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()
|
||||
@@ -141,7 +142,11 @@ 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.
|
||||
directFileRoot string
|
||||
// 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
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -155,9 +160,14 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
|
||||
|
||||
// NewLocalBackend returns a new LocalBackend that is ready to run,
|
||||
// but is not actually running.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
|
||||
//
|
||||
// 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) {
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
panic("ipn.NewLocalBackend: engine must not be nil")
|
||||
}
|
||||
if dialer == nil {
|
||||
dialer = new(tsdial.Dialer)
|
||||
}
|
||||
|
||||
osshare.SetFileSharingEnabled(false, logf)
|
||||
@@ -176,11 +186,13 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
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{}))
|
||||
|
||||
@@ -210,6 +222,11 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
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.
|
||||
@@ -221,6 +238,17 @@ 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 {
|
||||
@@ -389,33 +417,30 @@ 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,
|
||||
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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -600,6 +625,11 @@ 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() {
|
||||
@@ -1259,7 +1289,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
|
||||
return
|
||||
}
|
||||
|
||||
if apiSrv != nil && apiSrv.hasFilesWaiting() {
|
||||
if apiSrv.hasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
}
|
||||
|
||||
@@ -1668,7 +1698,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
}
|
||||
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done.
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
@@ -1676,6 +1706,10 @@ 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()
|
||||
@@ -1753,15 +1787,24 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.ServiceProto("peerapi4")
|
||||
proto := tailcfg.PeerAPI4
|
||||
if pln.ip.Is6() {
|
||||
proto = "peerapi6"
|
||||
proto = tailcfg.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
|
||||
}
|
||||
|
||||
@@ -1862,6 +1905,15 @@ 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)
|
||||
@@ -1960,12 +2012,32 @@ 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)
|
||||
@@ -1987,18 +2059,6 @@ 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
|
||||
@@ -2022,9 +2082,6 @@ 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
|
||||
@@ -2071,7 +2128,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
}
|
||||
varRoot := b.TailscaleVarRoot()
|
||||
if varRoot == "" {
|
||||
b.logf("peerapi disabled; no state directory")
|
||||
b.logf("Taildrop disabled; no state directory")
|
||||
return ""
|
||||
}
|
||||
baseDir := fmt.Sprintf("%s-uid-%d",
|
||||
@@ -2079,7 +2136,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("peerapi disabled; error making directory: %v", err)
|
||||
b.logf("Taildrop disabled; error making directory: %v", err)
|
||||
return ""
|
||||
}
|
||||
return dir
|
||||
@@ -2142,22 +2199,15 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
|
||||
fileRoot := b.fileRootLocked(selfNode.User)
|
||||
if fileRoot == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var tunName string
|
||||
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ge.GetInternals(); ok {
|
||||
tunName, _ = tunWrap.Name()
|
||||
}
|
||||
b.logf("peerapi starting without Taildrop directory configured")
|
||||
}
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
tunName: tunName,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
directFileDoFinalRename: b.directFileDoFinalRename,
|
||||
}
|
||||
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
|
||||
if r, ok := re.GetResolver(); ok {
|
||||
@@ -2640,6 +2690,7 @@ 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
|
||||
@@ -2744,9 +2795,6 @@ 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()
|
||||
}
|
||||
|
||||
@@ -2754,9 +2802,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -2764,9 +2809,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -2880,9 +2922,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
var p4, p6 uint16
|
||||
for _, s := range peer.Hostinfo.Services {
|
||||
switch s.Proto {
|
||||
case "peerapi4":
|
||||
case tailcfg.PeerAPI4:
|
||||
p4 = s.Port
|
||||
case "peerapi6":
|
||||
case tailcfg.PeerAPI6:
|
||||
p6 = s.Port
|
||||
}
|
||||
}
|
||||
@@ -3009,19 +3051,6 @@ 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()
|
||||
@@ -3053,3 +3082,55 @@ 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
|
||||
}
|
||||
|
||||
@@ -92,14 +92,14 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*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{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*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, eng)
|
||||
lb, err := NewLocalBackend(logf, "logid", store, nil, eng)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, e)
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -46,10 +46,13 @@ 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
|
||||
tunName string
|
||||
rootDir string // empty means file receiving unavailable
|
||||
selfNode *tailcfg.Node
|
||||
knownEmpty syncs.AtomicBool
|
||||
resolver *resolver.Resolver
|
||||
@@ -57,10 +60,17 @@ 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 GUI macOS version.
|
||||
// somewhere itself. This is used on the GUI macOS versions
|
||||
// and on Synology.
|
||||
// In directFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg".
|
||||
// from "foo.jpg.partial" to "foo.jpg" unless
|
||||
// directFileDoFinalRename is set.
|
||||
directFileMode bool
|
||||
|
||||
// directFileDoFinalRename is whether in directFileMode we
|
||||
// additionally move the *.direct file to its final name after
|
||||
// it's received.
|
||||
directFileDoFinalRename bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -77,6 +87,10 @@ const (
|
||||
deletedSuffix = ".deleted"
|
||||
)
|
||||
|
||||
func (s *peerAPIServer) canReceiveFiles() bool {
|
||||
return s != nil && s.rootDir != ""
|
||||
}
|
||||
|
||||
func validFilenameRune(r rune) bool {
|
||||
switch r {
|
||||
case '/':
|
||||
@@ -123,7 +137,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.rootDir == "" || s.directFileMode {
|
||||
if s == nil || s.rootDir == "" || s.directFileMode {
|
||||
return false
|
||||
}
|
||||
if s.knownEmpty.Get() {
|
||||
@@ -183,8 +197,11 @@ 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, errors.New("peerapi disabled; no storage configured")
|
||||
return nil, errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, nil
|
||||
@@ -248,6 +265,11 @@ 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
|
||||
@@ -263,8 +285,11 @@ func tryDeleteAgain(fullPath string) {
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if s == nil {
|
||||
return errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return errors.New("peerapi disabled; no storage configured")
|
||||
return errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return errors.New("deletes not allowed in direct mode")
|
||||
@@ -329,8 +354,11 @@ 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, errors.New("peerapi disabled; no storage configured")
|
||||
return nil, 0, errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, 0, errors.New("opens not allowed in direct mode")
|
||||
@@ -363,7 +391,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.tunName); err != nil {
|
||||
if err := initListenConfig(&lc, ip, ifState, s.b.dialer.TUNName()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
@@ -468,6 +496,9 @@ func (pln *peerAPIListener) serve() {
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
if addH2C != nil {
|
||||
addH2C(httpServer)
|
||||
}
|
||||
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
}
|
||||
}
|
||||
@@ -608,7 +639,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
@@ -680,7 +711,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
@@ -832,7 +863,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)
|
||||
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr, h.ps.b.allowExitNodeDNSProxyToServeName)
|
||||
if err != nil {
|
||||
h.logf("handleDNS fwd error: %v", err)
|
||||
if err := ctx.Err(); err != nil {
|
||||
@@ -849,7 +880,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(q)))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(res)))
|
||||
w.Write(res)
|
||||
}
|
||||
|
||||
@@ -918,14 +949,19 @@ 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
|
||||
if _, err := p.Start(res); err != nil {
|
||||
hdr, err := p.Start(res)
|
||||
if 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
|
||||
}
|
||||
|
||||
22
ipn/ipnlocal/peerapi_h2c.go
Normal file
22
ipn/ipnlocal/peerapi_h2c.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,8 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -21,7 +19,6 @@ import (
|
||||
|
||||
func init() {
|
||||
initListenConfig = initListenConfigNetworkExtension
|
||||
peerDialControlFunc = peerDialControlFuncNetworkExtension
|
||||
}
|
||||
|
||||
// initListenConfigNetworkExtension configures nc for listening on IP
|
||||
@@ -34,24 +31,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusInternalServerError),
|
||||
bodyContains("no rootdir"),
|
||||
bodyContains("Taildrop disabled; no storage directory"),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -284,7 +284,7 @@ func TestStateMachine(t *testing.T) {
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
cc := newMockControl(t)
|
||||
b, err := NewLocalBackend(logf, "logid", store, e)
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, 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), eng)
|
||||
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cc := newMockControl(t)
|
||||
|
||||
@@ -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,6 +48,7 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// Options is the configuration of the Tailscale node agent.
|
||||
@@ -651,7 +652,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, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
@@ -735,7 +736,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
}
|
||||
}
|
||||
|
||||
server, err := New(logf, logid, store, eng, serverModeUser, opts)
|
||||
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -748,8 +749,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, serverModeUser *user.User, opts Options) (*Server, error) {
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -758,6 +759,22 @@ 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 {
|
||||
@@ -851,14 +868,6 @@ 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
|
||||
@@ -1112,3 +1121,50 @@ 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)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -32,10 +33,11 @@ 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(socketPath, 0)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
t.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
@@ -72,6 +74,6 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
@@ -88,22 +88,23 @@ type PeerStatus struct {
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
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
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
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)
|
||||
|
||||
// Active is whether the node was recently active. The
|
||||
// definition is somewhat undefined but has historically and
|
||||
@@ -242,9 +243,6 @@ 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
|
||||
}
|
||||
@@ -275,6 +273,9 @@ 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
|
||||
}
|
||||
@@ -290,6 +291,9 @@ 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
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -28,7 +26,6 @@ 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"
|
||||
@@ -376,6 +373,25 @@ 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)
|
||||
@@ -423,7 +439,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
outReq.ContentLength = r.ContentLength
|
||||
|
||||
rp := httputil.NewSingleHostReverseProxy(dstURL)
|
||||
rp.Transport = getDialPeerTransport(h.b)
|
||||
rp.Transport = h.b.Dialer().PeerAPITransport()
|
||||
rp.ServeHTTP(w, outReq)
|
||||
}
|
||||
|
||||
@@ -457,26 +473,6 @@ 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
|
||||
|
||||
@@ -14,7 +14,8 @@ 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 {
|
||||
if s.Proto == "peerapi4" || s.Proto == "peerapi6" {
|
||||
switch s.Proto {
|
||||
case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
|
||||
return true
|
||||
}
|
||||
if s.Proto != tailcfg.TCP {
|
||||
|
||||
@@ -9,6 +9,7 @@ package filelogger
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@@ -26,30 +27,30 @@ const (
|
||||
maxFiles = 50
|
||||
)
|
||||
|
||||
// New returns a logf wrapper that appends to local disk log
|
||||
// New returns a Writer 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, logf logger.Logf) logger.Logf {
|
||||
func New(fileBasePrefix, logID string, inner *log.Logger) io.Writer {
|
||||
if runtime.GOOS != "windows" {
|
||||
panic("not yet supported on any platform except Windows")
|
||||
}
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
if inner == nil {
|
||||
panic("nil inner logger")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return logf
|
||||
inner.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return inner.Writer()
|
||||
}
|
||||
logf("local disk logdir: %v", dir)
|
||||
inner.Printf("local disk logdir: %v", dir)
|
||||
lfw := &logFileWriter{
|
||||
fileBasePrefix: fileBasePrefix,
|
||||
logID: logID,
|
||||
dir: dir,
|
||||
wrappedLogf: logf,
|
||||
wrappedLogf: inner.Printf,
|
||||
}
|
||||
return lfw.Logf
|
||||
return logger.FuncWriter(lfw.Logf)
|
||||
}
|
||||
|
||||
// logFileWriter is the state for the log writer & rotator.
|
||||
|
||||
@@ -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(lw)
|
||||
log.SetOutput(maybeWrapForPlatform(lw, cmdName, newc.PublicID.String()))
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.Long,
|
||||
|
||||
16
logpolicy/logpolicy_notwindows.go
Normal file
16
logpolicy/logpolicy_notwindows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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
|
||||
}
|
||||
26
logpolicy/logpolicy_windows.go
Normal file
26
logpolicy/logpolicy_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package dns
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -132,12 +134,20 @@ func isResolvedRunning() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
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
|
||||
}
|
||||
|
||||
func restartResolved() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
|
||||
// generated from the given configuration, creating a backup of its old state.
|
||||
//
|
||||
@@ -394,7 +404,12 @@ func (m *directManager) Close() error {
|
||||
}
|
||||
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -26,8 +26,8 @@ func TestParseIni(t *testing.T) {
|
||||
[network] # trailing comment
|
||||
generateResolvConf = false # trailing comment`,
|
||||
want: map[string]map[string]string{
|
||||
"automount": map[string]string{"enabled": "true", "root": "/mnt/"},
|
||||
"network": map[string]string{"generateResolvConf": "false"},
|
||||
"automount": {"enabled": "true", "root": "/mnt/"},
|
||||
"network": {"generateResolvConf": "false"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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"
|
||||
@@ -39,11 +40,14 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManagers created a new manager from the given config.
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, linkSel resolver.ForwardLinkSelector) *Manager {
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
m := &Manager{
|
||||
logf: logf,
|
||||
resolver: resolver.New(logf, linkMon, linkSel),
|
||||
resolver: resolver.New(logf, linkMon, linkSel, dialer),
|
||||
os: oscfg,
|
||||
}
|
||||
m.logf("using %T", m.os)
|
||||
@@ -230,7 +234,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
|
||||
logf("creating dns cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
dns := NewManager(logf, oscfg, nil, nil)
|
||||
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
|
||||
if err := dns.Down(); err != nil {
|
||||
logf("dns down: %v", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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"
|
||||
)
|
||||
@@ -398,7 +399,7 @@ func TestManager(t *testing.T) {
|
||||
SplitDNS: test.split,
|
||||
BaseConfig: test.bs,
|
||||
}
|
||||
m := NewManager(t.Logf, &f, nil, nil)
|
||||
m := NewManager(t.Logf, &f, nil, new(tsdial.Dialer), nil)
|
||||
m.resolver.TestOnlySetHook(f.SetResolver)
|
||||
|
||||
if err := m.Set(test.in); err != nil {
|
||||
|
||||
@@ -26,6 +26,7 @@ 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"
|
||||
@@ -159,7 +160,8 @@ type resolverAndDelay struct {
|
||||
type forwarder struct {
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon
|
||||
linkSel ForwardLinkSelector
|
||||
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
|
||||
dialer *tsdial.Dialer
|
||||
dohSem chan struct{}
|
||||
|
||||
ctx context.Context // good until Close
|
||||
@@ -205,11 +207,12 @@ func maxDoHInFlight(goos string) int {
|
||||
return 1000
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
|
||||
f := &forwarder{
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
linkMon: linkMon,
|
||||
linkSel: linkSel,
|
||||
dialer: dialer,
|
||||
responses: responses,
|
||||
dohSem: make(chan struct{}, maxDoHInFlight(runtime.GOOS)),
|
||||
}
|
||||
@@ -423,8 +426,7 @@ 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://") {
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
return nil, fmt.Errorf("http:// resolvers not supported yet")
|
||||
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "https://") {
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
@@ -580,9 +582,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 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 {
|
||||
// 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 {
|
||||
metricDNSFwd.Add(1)
|
||||
domain, err := nameFromQuery(query.bs)
|
||||
if err != nil {
|
||||
@@ -601,13 +603,12 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
|
||||
clampEDNSSize(query.bs, maxResponseBytes)
|
||||
|
||||
resolvers := f.resolvers(domain)
|
||||
if len(resolvers) == 0 {
|
||||
resolvers = backupResolvers
|
||||
}
|
||||
if len(resolvers) == 0 {
|
||||
metricDNSFwdErrorNoUpstream.Add(1)
|
||||
return errNoUpstreams
|
||||
resolvers = f.resolvers(domain)
|
||||
if len(resolvers) == 0 {
|
||||
metricDNSFwdErrorNoUpstream.Add(1)
|
||||
return errNoUpstreams
|
||||
}
|
||||
}
|
||||
|
||||
fq := &forwardQuery{
|
||||
|
||||
@@ -8,11 +8,14 @@ package resolver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -20,12 +23,16 @@ 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"
|
||||
)
|
||||
|
||||
@@ -186,6 +193,7 @@ 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
|
||||
@@ -217,7 +225,10 @@ 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) *Resolver {
|
||||
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *Resolver {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
r := &Resolver{
|
||||
logf: logger.WithPrefix(logf, "resolver: "),
|
||||
linkMon: linkMon,
|
||||
@@ -226,8 +237,9 @@ func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *R
|
||||
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)
|
||||
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel, dialer)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -303,48 +315,84 @@ 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.
|
||||
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort) (res []byte, err error) {
|
||||
metricDNSQueryForPeer.Add(1)
|
||||
//
|
||||
// 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)
|
||||
ch := make(chan packet, 1)
|
||||
|
||||
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
|
||||
if err == errNoUpstreams {
|
||||
// Handle to the system resolver.
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// Assume for now that we don't have an upstream because
|
||||
// they're using systemd-resolved and we're in Split DNS mode
|
||||
// where we don't know the base config.
|
||||
//
|
||||
// TODO(bradfitz): this is a lazy assumption. Do better, and
|
||||
// maybe move the HandleExitNodeDNSQuery method to the dns.Manager
|
||||
// instead? But this works for now.
|
||||
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolverAndDelay{
|
||||
name: dnstype.Resolver{
|
||||
Addr: "127.0.0.1:53",
|
||||
},
|
||||
})
|
||||
default:
|
||||
// TODO(bradfitz): if we're on an exit node
|
||||
// on, say, Windows, we need to parse the DNS
|
||||
// packet in q and call OS-native APIs for
|
||||
// each question. But we'll want to strip out
|
||||
// questions for MagicDNS names probably, so
|
||||
// they don't loop back into
|
||||
// 100.100.100.100. We don't want to resolve
|
||||
// MagicDNS names across Tailnets once we
|
||||
// permit sharing exit nodes.
|
||||
//
|
||||
// For now, just return an error.
|
||||
resp := parseExitNodeQuery(q)
|
||||
if resp == nil {
|
||||
return nil, errors.New("bad query")
|
||||
}
|
||||
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:
|
||||
@@ -357,6 +405,159 @@ 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).
|
||||
@@ -504,10 +705,27 @@ 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 is the response to an A, AAAA, or ALL query.
|
||||
IP netaddr.IP
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var dnsParserPool = &sync.Pool{
|
||||
@@ -538,6 +756,7 @@ 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 {
|
||||
@@ -582,6 +801,16 @@ 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 {
|
||||
@@ -601,6 +830,83 @@ 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
|
||||
@@ -611,6 +917,14 @@ 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 {
|
||||
@@ -637,13 +951,24 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
|
||||
switch resp.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -825,6 +1150,37 @@ 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")
|
||||
@@ -837,8 +1193,10 @@ var (
|
||||
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
|
||||
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
|
||||
|
||||
metricDNSQueryForPeer = clientmetric.NewCounter("dns_query_peerapi")
|
||||
metricDNSQueryForPeerError = clientmetric.NewCounter("dns_query_peerapi_error")
|
||||
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")
|
||||
|
||||
metricDNSFwd = clientmetric.NewCounter("dns_query_fwd")
|
||||
metricDNSFwdDropBonjour = clientmetric.NewCounter("dns_query_fwd_drop_bonjour")
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -179,6 +180,129 @@ 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")
|
||||
|
||||
@@ -6,18 +6,25 @@ 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"
|
||||
@@ -34,14 +41,16 @@ var (
|
||||
|
||||
var dnsCfg = Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"test1.ipn.dev.": []netaddr.IP{testipv4},
|
||||
"test2.ipn.dev.": []netaddr.IP{testipv6},
|
||||
"test1.ipn.dev.": {testipv4},
|
||||
"test2.ipn.dev.": {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{
|
||||
@@ -308,7 +317,7 @@ func TestRDNSNameToIPv6(t *testing.T) {
|
||||
}
|
||||
|
||||
func newResolver(t testing.TB) *Resolver {
|
||||
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */)
|
||||
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */, new(tsdial.Dialer))
|
||||
}
|
||||
|
||||
func TestResolveLocal(t *testing.T) {
|
||||
@@ -1062,7 +1071,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 {
|
||||
@@ -1092,3 +1101,383 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
net/dns/utf.go
Normal file
56
net/dns/utf.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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)))
|
||||
}
|
||||
25
net/dns/utf_test.go
Normal file
25
net/dns/utf_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,29 +26,7 @@ func wslDistros() ([]string, error) {
|
||||
return nil, fmt.Errorf("%v: %q", err, string(b))
|
||||
}
|
||||
|
||||
// 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")
|
||||
lines := strings.Split(string(b), "\n")
|
||||
if len(lines) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -66,19 +44,6 @@ 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 {
|
||||
@@ -193,7 +158,8 @@ 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))
|
||||
if ee, _ := err.(*exec.ExitError); ee != nil && ee.ExitCode() == 1 {
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return b, err
|
||||
@@ -225,7 +191,10 @@ func wslCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
cmd.Stdout = buf
|
||||
cmd.Stderr = buf
|
||||
err := wslRun(cmd)
|
||||
return buf.Bytes(), err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return maybeUnUTF16(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
func wslRun(cmd *exec.Cmd) (err error) {
|
||||
|
||||
314
net/dnscache/messagecache.go
Normal file
314
net/dnscache/messagecache.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// 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()
|
||||
}
|
||||
292
net/dnscache/messagecache_test.go
Normal file
292
net/dnscache/messagecache_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// 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" }
|
||||
@@ -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 {
|
||||
|
||||
@@ -60,9 +60,6 @@ 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)
|
||||
}
|
||||
@@ -75,9 +72,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@ 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.
|
||||
@@ -49,5 +59,9 @@ 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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
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.
|
||||
@@ -42,3 +48,120 @@ 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))
|
||||
}
|
||||
|
||||
80
net/packet/icmp6_test.go
Normal file
80
net/packet/icmp6_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -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) error {
|
||||
func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
@@ -72,6 +72,6 @@ func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
buf[36] = 0
|
||||
buf[37] = 0
|
||||
buf[38] = 0
|
||||
buf[39] = 17 // NextProto
|
||||
buf[39] = byte(proto) // NextProto
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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 packet types.
|
||||
// It performs extremely simple packet decoding for basic IPv4 and IPv6 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,9 +339,6 @@ 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]),
|
||||
@@ -349,10 +346,15 @@ func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
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 {
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.Src.Port(),
|
||||
@@ -410,7 +412,7 @@ func (q *Parsed) IsEchoRequest() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
|
||||
// IsEchoResponse reports whether q is an IPv4 ICMP Echo Response.
|
||||
func (q *Parsed) IsEchoResponse() bool {
|
||||
switch q.IPProto {
|
||||
case ipproto.ICMPv4:
|
||||
|
||||
@@ -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)
|
||||
h.IP6Header.marshalPseudo(buf, ipproto.UDP)
|
||||
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
@@ -63,11 +63,18 @@ 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
|
||||
|
||||
@@ -19,6 +19,10 @@ 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
|
||||
@@ -26,8 +30,14 @@ const (
|
||||
|
||||
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
||||
|
||||
pcpCodeOK = 0
|
||||
pcpCodeNotAuthorized = 2
|
||||
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
|
||||
|
||||
pcpOpReply = 0x80 // OR'd into request's op code on response
|
||||
pcpOpAnnounce = 0
|
||||
@@ -140,7 +150,7 @@ func pcpAnnounceRequest(myIP netaddr.IP) []byte {
|
||||
|
||||
type pcpResponse struct {
|
||||
OpCode uint8
|
||||
ResultCode uint8
|
||||
ResultCode pcpResultCode
|
||||
Lifetime uint32
|
||||
Epoch uint32
|
||||
}
|
||||
@@ -150,7 +160,7 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
|
||||
return
|
||||
}
|
||||
res.OpCode = b[1]
|
||||
res.ResultCode = b[3]
|
||||
res.ResultCode = pcpResultCode(b[3])
|
||||
res.Lifetime = binary.BigEndian.Uint32(b[4:])
|
||||
res.Epoch = binary.BigEndian.Uint32(b[8:])
|
||||
return res, true
|
||||
|
||||
37
net/portmapper/pcpresultcode_string.go
Normal file
37
net/portmapper/pcpresultcode_string.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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) + ")"
|
||||
}
|
||||
}
|
||||
32
net/portmapper/pmpresultcode_string.go
Normal file
32
net/portmapper/pmpresultcode_string.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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]]
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
// Debug knobs for "tailscaled debug --portmap".
|
||||
@@ -563,6 +564,8 @@ 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.
|
||||
@@ -685,11 +688,13 @@ 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() {
|
||||
@@ -734,6 +739,7 @@ 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)
|
||||
}
|
||||
@@ -759,11 +765,15 @@ 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 {
|
||||
c.logf("unrecognized UPnP discovery response; ignoring")
|
||||
metricUPnPParseErr.Add(1)
|
||||
c.logf("unrecognized UPnP discovery response; ignoring: %v", err)
|
||||
continue
|
||||
}
|
||||
metricUPnPOK.Add(1)
|
||||
c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
@@ -771,10 +781,12 @@ 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
|
||||
@@ -785,25 +797,35 @@ 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()
|
||||
@@ -812,11 +834,20 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
case pmpCodeNotAuthorized, pmpCodeNetworkFailure, pmpCodeOutOfResources:
|
||||
// Normal failures.
|
||||
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)
|
||||
c.logf("PMP probe failed due result code: %+v", pres)
|
||||
continue
|
||||
}
|
||||
metricPMPUnhandledResponseCode.Add(1)
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
@@ -835,3 +866,74 @@ 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")
|
||||
)
|
||||
|
||||
145
net/proxymux/mux.go
Normal file
145
net/proxymux/mux.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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
|
||||
}
|
||||
172
net/proxymux/mux_test.go
Normal file
172
net/proxymux/mux_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,79 +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 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)
|
||||
}
|
||||
115
net/tsdial/dnsmap.go
Normal file
115
net/tsdial/dnsmap.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netstack
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -19,7 +19,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
want DNSMap
|
||||
want dnsMap
|
||||
}{
|
||||
{
|
||||
name: "self",
|
||||
@@ -30,7 +30,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
},
|
||||
want: DNSMap{
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
},
|
||||
@@ -59,7 +59,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: DNSMap{
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
"a": ip("100.0.0.201"),
|
||||
@@ -91,7 +91,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: DNSMap{
|
||||
want: dnsMap{
|
||||
"foo": ip("100::123"),
|
||||
"foo.tailnet": ip("100::123"),
|
||||
"a": ip("100::201"),
|
||||
@@ -103,7 +103,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := DNSMapFromNetworkMap(tt.nm)
|
||||
got := dnsMapFromNetworkMap(tt.nm)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch:\n got %v\nwant %v\n", got, tt.want)
|
||||
}
|
||||
101
net/tsdial/dohclient.go
Normal file
101
net/tsdial/dohclient.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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" }
|
||||
32
net/tsdial/dohclient_test.go
Normal file
32
net/tsdial/dohclient_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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)
|
||||
}
|
||||
43
net/tsdial/peerapi_macios_ext.go
Normal file
43
net/tsdial/peerapi_macios_ext.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
281
net/tsdial/tsdial.go
Normal file
281
net/tsdial/tsdial.go
Normal file
@@ -0,0 +1,281 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -15,13 +15,13 @@ func TestParsePort(t *testing.T) {
|
||||
expect int
|
||||
}
|
||||
tests := []InOut{
|
||||
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},
|
||||
{"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},
|
||||
}
|
||||
|
||||
for _, io := range tests {
|
||||
|
||||
@@ -48,7 +48,9 @@ func TestBasics(t *testing.T) {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c, err := Connect(sock, port)
|
||||
s := DefaultConnectionStrategy(sock)
|
||||
s.UsePort(port)
|
||||
c, err := Connect(s)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -57,10 +57,65 @@ func tailscaledStillStarting() bool {
|
||||
return tailscaledProcExists()
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
// 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) {
|
||||
for {
|
||||
c, err := connect(path, port)
|
||||
c, err := connect(s)
|
||||
if err != nil && tailscaledStillStarting() {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
continue
|
||||
|
||||
@@ -17,6 +17,6 @@ func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error)
|
||||
return ln, 1, err
|
||||
}
|
||||
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
func connect(_ *ConnectionStrategy) (net.Conn, error) {
|
||||
return memconn.Dial("memu", memName)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,5 @@ func init() {
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,19 +23,19 @@ import (
|
||||
)
|
||||
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
if runtime.GOOS == "js" {
|
||||
return nil, errors.New("safesocket.Connect not yet implemented on js/wasm")
|
||||
}
|
||||
if runtime.GOOS == "darwin" && path == "" && port == 0 {
|
||||
if runtime.GOOS == "darwin" && s.fallback && s.path == "" && s.port == 0 {
|
||||
return connectMacOSAppSandbox()
|
||||
}
|
||||
pipe, err := net.Dial("unix", path)
|
||||
pipe, err := net.Dial("unix", s.path)
|
||||
if err != nil {
|
||||
if runtime.GOOS == "darwin" {
|
||||
if runtime.GOOS == "darwin" && s.fallback {
|
||||
extConn, extErr := connectMacOSAppSandbox()
|
||||
if extErr != nil {
|
||||
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", path, err, extErr)
|
||||
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", s.path, err, extErr)
|
||||
}
|
||||
return extConn, nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
ubuntu|pop|neon|zorin|elementary|linuxmint)
|
||||
OS="ubuntu"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
@@ -68,6 +68,23 @@ 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"
|
||||
@@ -94,6 +111,11 @@ main() {
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
rocky)
|
||||
OS="fedora"
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
amzn)
|
||||
OS="amazon-linux"
|
||||
VERSION="$VERSION_ID"
|
||||
@@ -386,6 +408,10 @@ 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)
|
||||
@@ -399,7 +425,7 @@ main() {
|
||||
dnf)
|
||||
set -x
|
||||
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO dnf install tailscale
|
||||
$SUDO dnf install -y tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
|
||||
@@ -379,18 +379,49 @@ 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")
|
||||
TCP = ServiceProto("tcp")
|
||||
UDP = ServiceProto("udp")
|
||||
PeerAPI4 = ServiceProto("peerapi4")
|
||||
PeerAPI6 = ServiceProto("peerapi6")
|
||||
PeerAPIDNS = ServiceProto("peerapi-dns-proxy")
|
||||
)
|
||||
|
||||
// Service represents a service running on a node.
|
||||
type Service struct {
|
||||
_ structs.Incomparable
|
||||
Proto ServiceProto // TCP or UDP
|
||||
Port uint16 // port number service is listening on
|
||||
Description string `json:",omitempty"` // text description of service
|
||||
_ 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"`
|
||||
|
||||
// TODO(apenwarr): allow advertising services on subnet IPs?
|
||||
// TODO(apenwarr): add "tags" here for each service?
|
||||
}
|
||||
@@ -907,6 +938,21 @@ 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.
|
||||
|
||||
@@ -208,20 +208,22 @@ 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
|
||||
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
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of RegisterResponse.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package tsnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -20,12 +21,14 @@ 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"
|
||||
@@ -114,9 +117,11 @@ 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
|
||||
@@ -127,7 +132,7 @@ func (s *Server) start() error {
|
||||
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn)
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
@@ -136,6 +141,13 @@ 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)
|
||||
@@ -144,7 +156,7 @@ func (s *Server) start() error {
|
||||
}
|
||||
logid := "tslib-TODO"
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"go4.org/mem"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -42,38 +43,68 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// 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()),
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
func build(t testing.TB, outDir string, targets ...string) {
|
||||
buildMu.Lock()
|
||||
defer buildMu.Unlock()
|
||||
// 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())
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
|
||||
// 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())
|
||||
}
|
||||
|
||||
goBin := findGo(t)
|
||||
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
|
||||
}
|
||||
cmd := exec.Command(goBin, "install")
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
@@ -82,7 +113,7 @@ func build(t testing.TB, outDir string, targets ...string) {
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
|
||||
errOut, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(string(errOut), "when GOBIN is set") {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
@@ -91,25 +122,25 @@ func build(t testing.TB, outDir string, targets ...string) {
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
return fmt.Errorf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
}
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
return fmt.Errorf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
}
|
||||
|
||||
func findGo(t testing.TB) string {
|
||||
func findGo() (string, error) {
|
||||
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
|
||||
if fi, err := os.Stat(goBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("failed to find go at %v", goBin)
|
||||
return "", fmt.Errorf("failed to find go at %v", goBin)
|
||||
}
|
||||
t.Fatalf("looking for go binary: %v", err)
|
||||
return "", fmt.Errorf("looking for go binary: %v", err)
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
|
||||
return "", fmt.Errorf("%v is unexpected %v", goBin, fi.Mode())
|
||||
}
|
||||
return goBin
|
||||
return goBin, nil
|
||||
}
|
||||
|
||||
func exe() string {
|
||||
@@ -239,12 +270,15 @@ 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
|
||||
body, err = smallzstd.NewDecoder(body)
|
||||
var dec *zstd.Decoder
|
||||
dec, 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)
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ func TestMain(m *testing.M) {
|
||||
os.Setenv("TS_DISABLE_UPNP", "true")
|
||||
flag.Parse()
|
||||
v := m.Run()
|
||||
CleanupBinaries()
|
||||
if v != 0 {
|
||||
os.Exit(v)
|
||||
}
|
||||
@@ -62,17 +63,12 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
func TestOneNodeUpNoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
n1.AwaitResponding(t)
|
||||
n1.MustUp()
|
||||
|
||||
@@ -86,15 +82,10 @@ func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
|
||||
func TestOneNodeExpiredKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
n1.AwaitResponding(t)
|
||||
n1.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
@@ -127,14 +118,10 @@ func TestOneNodeExpiredKey(t *testing.T) {
|
||||
|
||||
func TestCollectPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n := newTestNode(t, env)
|
||||
|
||||
cmd := exec.Command(n.env.Binaries.Daemon, "--cleanup")
|
||||
cmd := exec.Command(env.daemon, "--cleanup")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TS_PLEASE_PANIC=1",
|
||||
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
|
||||
@@ -143,7 +130,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(n.env.Binaries.Daemon, "--cleanup")
|
||||
cmd = exec.Command(env.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)
|
||||
@@ -162,15 +149,10 @@ func TestCollectPanic(t *testing.T) {
|
||||
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
|
||||
func TestStateSavedOnStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
n1.AwaitResponding(t)
|
||||
n1.MustUp()
|
||||
|
||||
@@ -201,18 +183,14 @@ func TestStateSavedOnStart(t *testing.T) {
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
func TestOneNodeUp_Auth(t *testing.T) {
|
||||
func TestOneNodeUpAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins, configureControl(func(control *testcontrol.Server) {
|
||||
env := newTestEnv(t, 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)
|
||||
|
||||
@@ -250,21 +228,16 @@ func TestOneNodeUp_Auth(t *testing.T) {
|
||||
|
||||
func TestTwoNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
env := newTestEnv(t)
|
||||
|
||||
// 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)
|
||||
@@ -301,14 +274,9 @@ func TestTwoNodes(t *testing.T) {
|
||||
|
||||
func TestNodeAddressIPFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
@@ -332,14 +300,9 @@ func TestNodeAddressIPFields(t *testing.T) {
|
||||
|
||||
func TestAddPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
n1.StartDaemon(t)
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
@@ -391,15 +354,10 @@ func TestAddPingRequest(t *testing.T) {
|
||||
// be connected to control.
|
||||
func TestNoControlConnWhenDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
n1.AwaitResponding(t)
|
||||
|
||||
// Come up the first time.
|
||||
@@ -413,7 +371,6 @@ func TestNoControlConnWhenDown(t *testing.T) {
|
||||
|
||||
env.LogCatcher.Reset()
|
||||
d2 := n1.StartDaemon(t)
|
||||
defer d2.Kill()
|
||||
n1.AwaitResponding(t)
|
||||
|
||||
st := n1.MustStatus(t)
|
||||
@@ -438,16 +395,11 @@ func TestNoControlConnWhenDown(t *testing.T) {
|
||||
// without the GUI to kick off a Start.
|
||||
func TestOneNodeUpWindowsStyle(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.upFlagGOOS = "windows"
|
||||
|
||||
d1 := n1.StartDaemonAsIPNGOOS(t, "windows")
|
||||
defer d1.Kill()
|
||||
n1.AwaitResponding(t)
|
||||
n1.MustUp("--unattended")
|
||||
|
||||
@@ -460,8 +412,9 @@ 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
|
||||
Binaries *Binaries
|
||||
t testing.TB
|
||||
cli string
|
||||
daemon string
|
||||
|
||||
LogCatcher *LogCatcher
|
||||
LogCatcherServer *httptest.Server
|
||||
@@ -483,11 +436,9 @@ func (f configureControl) modifyTestEnv(te *testEnv) {
|
||||
f(te.Control)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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 {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("not tested/working on Windows yet")
|
||||
}
|
||||
@@ -500,7 +451,8 @@ func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
|
||||
trafficTrap := new(trafficTrap)
|
||||
e := &testEnv{
|
||||
t: t,
|
||||
Binaries: bins,
|
||||
cli: TailscaleBinary(t),
|
||||
daemon: TailscaledBinary(t),
|
||||
LogCatcher: logc,
|
||||
LogCatcherServer: httptest.NewServer(logc),
|
||||
Control: control,
|
||||
@@ -512,21 +464,19 @@ func newTestEnv(t testing.TB, bins *Binaries, 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.
|
||||
@@ -681,10 +631,6 @@ 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()
|
||||
@@ -696,14 +642,14 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
|
||||
}
|
||||
}
|
||||
|
||||
// StartDaemon starts the node's tailscaled, failing if it fails to
|
||||
// start.
|
||||
// StartDaemon starts the node's tailscaled, failing if it fails to start.
|
||||
// StartDaemon ensures that the process will exit when the test completes.
|
||||
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.Binaries.Daemon,
|
||||
cmd := exec.Command(n.env.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+n.stateFile,
|
||||
"--socket="+n.sockFile,
|
||||
@@ -724,6 +670,7 @@ 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,
|
||||
}
|
||||
@@ -736,8 +683,11 @@ func (n *testNode) MustUp(extraArgs ...string) {
|
||||
"--login-server=" + n.env.ControlServer.URL,
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
t.Logf("Running %v ...", args)
|
||||
if b, err := n.Tailscale(args...).CombinedOutput(); err != nil {
|
||||
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.Fatalf("up: %v, %v", string(b), err)
|
||||
}
|
||||
}
|
||||
@@ -753,8 +703,10 @@ 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(n.sockFile, safesocket.WindowsLocalPort)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -841,7 +793,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.Binaries.CLI, "--socket="+n.sockFile)
|
||||
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile)
|
||||
cmd.Args = append(cmd.Args, arg...)
|
||||
cmd.Dir = n.dir
|
||||
cmd.Env = append(os.Environ(),
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
_ "tailscale.com/net/interfaces"
|
||||
_ "tailscale.com/net/netns"
|
||||
_ "tailscale.com/net/portmapper"
|
||||
_ "tailscale.com/net/socks5/tssocks"
|
||||
_ "tailscale.com/net/proxymux"
|
||||
_ "tailscale.com/net/socks5"
|
||||
_ "tailscale.com/net/tsdial"
|
||||
_ "tailscale.com/net/tshttpproxy"
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
_ "tailscale.com/net/interfaces"
|
||||
_ "tailscale.com/net/netns"
|
||||
_ "tailscale.com/net/portmapper"
|
||||
_ "tailscale.com/net/socks5/tssocks"
|
||||
_ "tailscale.com/net/proxymux"
|
||||
_ "tailscale.com/net/socks5"
|
||||
_ "tailscale.com/net/tsdial"
|
||||
_ "tailscale.com/net/tshttpproxy"
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
_ "tailscale.com/net/interfaces"
|
||||
_ "tailscale.com/net/netns"
|
||||
_ "tailscale.com/net/portmapper"
|
||||
_ "tailscale.com/net/socks5/tssocks"
|
||||
_ "tailscale.com/net/proxymux"
|
||||
_ "tailscale.com/net/socks5"
|
||||
_ "tailscale.com/net/tsdial"
|
||||
_ "tailscale.com/net/tshttpproxy"
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
_ "tailscale.com/net/interfaces"
|
||||
_ "tailscale.com/net/netns"
|
||||
_ "tailscale.com/net/portmapper"
|
||||
_ "tailscale.com/net/socks5/tssocks"
|
||||
_ "tailscale.com/net/proxymux"
|
||||
_ "tailscale.com/net/socks5"
|
||||
_ "tailscale.com/net/tsdial"
|
||||
_ "tailscale.com/net/tshttpproxy"
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
|
||||
@@ -26,7 +26,9 @@ import (
|
||||
_ "tailscale.com/net/interfaces"
|
||||
_ "tailscale.com/net/netns"
|
||||
_ "tailscale.com/net/portmapper"
|
||||
_ "tailscale.com/net/socks5/tssocks"
|
||||
_ "tailscale.com/net/proxymux"
|
||||
_ "tailscale.com/net/socks5"
|
||||
_ "tailscale.com/net/tsdial"
|
||||
_ "tailscale.com/net/tshttpproxy"
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
|
||||
@@ -35,7 +35,9 @@ import (
|
||||
type Harness struct {
|
||||
testerDialer proxy.Dialer
|
||||
testerDir string
|
||||
bins *integration.Binaries
|
||||
binaryDir string
|
||||
cli string
|
||||
daemon string
|
||||
pubKey string
|
||||
signer ssh.Signer
|
||||
cs *testcontrol.Server
|
||||
@@ -134,11 +136,11 @@ func newHarness(t *testing.T) *Harness {
|
||||
loginServer := fmt.Sprintf("http://%s", ln.Addr())
|
||||
t.Logf("loginServer: %s", loginServer)
|
||||
|
||||
bins := integration.BuildTestBinaries(t)
|
||||
|
||||
h := &Harness{
|
||||
pubKey: string(pubkey),
|
||||
bins: bins,
|
||||
binaryDir: integration.BinaryDir(t),
|
||||
cli: integration.TailscaleBinary(t),
|
||||
daemon: integration.TailscaledBinary(t),
|
||||
signer: signer,
|
||||
loginServerURL: loginServer,
|
||||
cs: cs,
|
||||
@@ -146,7 +148,7 @@ func newHarness(t *testing.T) *Harness {
|
||||
ipMap: ipMap,
|
||||
}
|
||||
|
||||
h.makeTestNode(t, bins, loginServer)
|
||||
h.makeTestNode(t, loginServer)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -156,7 +158,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte {
|
||||
|
||||
args = append([]string{"--socket=" + filepath.Join(h.testerDir, "sock")}, args...)
|
||||
|
||||
cmd := exec.Command(h.bins.CLI, args...)
|
||||
cmd := exec.Command(h.cli, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -169,7 +171,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte {
|
||||
// enables us to make connections to and from the tailscale network being
|
||||
// tested. This mutates the Harness to allow tests to dial into the tailscale
|
||||
// network as well as control the tester's tailscaled.
|
||||
func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, controlURL string) {
|
||||
func (h *Harness) makeTestNode(t *testing.T, controlURL string) {
|
||||
dir := t.TempDir()
|
||||
h.testerDir = dir
|
||||
|
||||
@@ -179,7 +181,7 @@ func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, control
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
bins.Daemon,
|
||||
h.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+filepath.Join(dir, "state.json"),
|
||||
"--socket="+filepath.Join(dir, "sock"),
|
||||
@@ -222,7 +224,7 @@ outer:
|
||||
}
|
||||
}
|
||||
|
||||
run(t, dir, bins.CLI,
|
||||
run(t, dir, h.cli,
|
||||
"--socket="+filepath.Join(dir, "sock"),
|
||||
"up",
|
||||
"--login-server="+controlURL,
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -153,15 +152,15 @@ in {
|
||||
systemd.services.tailscaled.environment."TS_LOG_TARGET" = "{{.LogTarget}}";
|
||||
}`
|
||||
|
||||
func copyUnit(t *testing.T, bins *integration.Binaries) {
|
||||
func (h *Harness) copyUnit(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile("../../../cmd/tailscaled/tailscaled.service")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.MkdirAll(filepath.Join(bins.Dir, "systemd"), 0755)
|
||||
err = os.WriteFile(filepath.Join(bins.Dir, "systemd", "tailscaled.service"), data, 0666)
|
||||
os.MkdirAll(filepath.Join(h.binaryDir, "systemd"), 0755)
|
||||
err = os.WriteFile(filepath.Join(h.binaryDir, "systemd", "tailscaled.service"), data, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -172,7 +171,7 @@ func (h *Harness) makeNixOSImage(t *testing.T, d Distro, cdir string) string {
|
||||
t.Skip("https://github.com/NixOS/nixpkgs/issues/131098")
|
||||
}
|
||||
|
||||
copyUnit(t, h.bins)
|
||||
h.copyUnit(t)
|
||||
dir := t.TempDir()
|
||||
fname := filepath.Join(dir, d.Name+".nix")
|
||||
fout, err := os.Create(fname)
|
||||
@@ -185,7 +184,7 @@ func (h *Harness) makeNixOSImage(t *testing.T, d Distro, cdir string) string {
|
||||
BinPath string
|
||||
LogTarget string
|
||||
}{
|
||||
BinPath: h.bins.Dir,
|
||||
BinPath: h.binaryDir,
|
||||
LogTarget: h.loginServerURL,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ package vms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -23,10 +24,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -171,21 +172,19 @@ func fetchFromS3(t *testing.T, fout *os.File, d Distro) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
})
|
||||
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1"))
|
||||
if err != nil {
|
||||
t.Logf("can't make AWS session: %v", err)
|
||||
t.Logf("can't load AWS credentials: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
dler := s3manager.NewDownloader(sess, func(d *s3manager.Downloader) {
|
||||
dler := manager.NewDownloader(s3.NewFromConfig(cfg), func(d *manager.Downloader) {
|
||||
d.PartSize = 64 * 1024 * 1024 // 64MB per part
|
||||
})
|
||||
|
||||
t.Logf("fetching s3://%s/%s", bucketName, d.SHA256Sum)
|
||||
|
||||
_, err = dler.Download(fout, &s3.GetObjectInput{
|
||||
_, err = dler.Download(context.TODO(), fout, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(d.SHA256Sum),
|
||||
})
|
||||
@@ -291,7 +290,6 @@ func checkCachedImageHash(t *testing.T, d Distro, cacheDir string) string {
|
||||
}
|
||||
|
||||
func (h *Harness) copyBinaries(t *testing.T, d Distro, conn *ssh.Client) {
|
||||
bins := h.bins
|
||||
if strings.HasPrefix(d.Name, "nixos") {
|
||||
return
|
||||
}
|
||||
@@ -306,8 +304,8 @@ func (h *Harness) copyBinaries(t *testing.T, d Distro, conn *ssh.Client) {
|
||||
mkdir(t, cli, "/etc/default")
|
||||
mkdir(t, cli, "/var/lib/tailscale")
|
||||
|
||||
copyFile(t, cli, bins.Daemon, "/usr/sbin/tailscaled")
|
||||
copyFile(t, cli, bins.CLI, "/usr/bin/tailscale")
|
||||
copyFile(t, cli, h.daemon, "/usr/sbin/tailscaled")
|
||||
copyFile(t, cli, h.cli, "/usr/bin/tailscale")
|
||||
|
||||
// TODO(Xe): revisit this assumption before it breaks the test.
|
||||
copyFile(t, cli, "../../../cmd/tailscaled/tailscaled.defaults", "/etc/default/tailscaled")
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"golang.org/x/sync/semaphore"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -52,6 +53,13 @@ var (
|
||||
}()
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
v := m.Run()
|
||||
integration.CleanupBinaries()
|
||||
os.Exit(v)
|
||||
}
|
||||
|
||||
func TestDownloadImages(t *testing.T) {
|
||||
if !*runVMTests {
|
||||
t.Skip("not running integration tests (need --run-vm-tests)")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// This file exists just so go mod tidy won't remove
|
||||
// staticcheck's module from our go.mod.
|
||||
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package tstest
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
)
|
||||
|
||||
// Logf is the basic Tailscale logger type: a printf-like func.
|
||||
@@ -25,6 +27,27 @@ import (
|
||||
// Logf functions must be safe for concurrent use.
|
||||
type Logf func(format string, args ...interface{})
|
||||
|
||||
// A Context is a context.Context that should contain a custom log function, obtainable from FromContext.
|
||||
// If no log function is present, FromContext will return log.Printf.
|
||||
// To construct a Context, use Add
|
||||
type Context context.Context
|
||||
|
||||
type logfKey struct{}
|
||||
|
||||
// FromContext extracts a log function from ctx.
|
||||
func FromContext(ctx Context) Logf {
|
||||
v := ctx.Value(logfKey{})
|
||||
if v == nil {
|
||||
return log.Printf
|
||||
}
|
||||
return v.(Logf)
|
||||
}
|
||||
|
||||
// Ctx constructs a Context from ctx with fn as its custom log function.
|
||||
func Ctx(ctx context.Context, fn Logf) Context {
|
||||
return context.WithValue(ctx, logfKey{}, fn)
|
||||
}
|
||||
|
||||
// WithPrefix wraps f, prefixing each format with the provided prefix.
|
||||
func WithPrefix(f Logf, prefix string) Logf {
|
||||
return func(format string, args ...interface{}) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user