Compare commits
83 Commits
tom/integr
...
v1.24.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dce2409b15 | ||
|
|
258f251af3 | ||
|
|
4a1f4c2cad | ||
|
|
5651fa1e60 | ||
|
|
73f169e8f5 | ||
|
|
759a2bd546 | ||
|
|
c0746cf25c | ||
|
|
5ff23cb1ce | ||
|
|
497fab5640 | ||
|
|
2226bd99fa | ||
|
|
465642b249 | ||
|
|
a51123022a | ||
|
|
f90052c86c | ||
|
|
4be1222701 | ||
|
|
70a6d87b16 | ||
|
|
de1ebee14f | ||
|
|
3a7f71df63 | ||
|
|
b16e27db9e | ||
|
|
f0e71f4a20 | ||
|
|
2265587d38 | ||
|
|
78fededaa5 | ||
|
|
910ae68e0b | ||
|
|
c2eff20008 | ||
|
|
700bd37730 | ||
|
|
90b5f6286c | ||
|
|
db70774685 | ||
|
|
37c94c07cd | ||
|
|
a364bf2b62 | ||
|
|
c994eba763 | ||
|
|
31094d557b | ||
|
|
337c77964b | ||
|
|
8ac4d52b59 | ||
|
|
89832c1a95 | ||
|
|
695f8a1d7e | ||
|
|
53588f632d | ||
|
|
df26c63793 | ||
|
|
8d6793fd70 | ||
|
|
f7cb6630e7 | ||
|
|
5b4154342e | ||
|
|
7a097ccc83 | ||
|
|
2b8b887d55 | ||
|
|
13f75b9667 | ||
|
|
c2b907c965 | ||
|
|
61868f281e | ||
|
|
db7da6622a | ||
|
|
d413850bd7 | ||
|
|
f74ee80abe | ||
|
|
14d077fc3a | ||
|
|
a2c330c496 | ||
|
|
136f30fc92 | ||
|
|
8e40bfc6ea | ||
|
|
1b89662eff | ||
|
|
cf9b9a7fec | ||
|
|
8b81254992 | ||
|
|
0ce67ccda6 | ||
|
|
fc2f628d4c | ||
|
|
33fa43252e | ||
|
|
c8f4dfc8c0 | ||
|
|
cc575fe4d6 | ||
|
|
e3a4952527 | ||
|
|
d9efbd97cb | ||
|
|
c13be0c509 | ||
|
|
91a187bf87 | ||
|
|
a04eebf59f | ||
|
|
d201d217df | ||
|
|
24cd26534f | ||
|
|
9f1dd716e8 | ||
|
|
ecea6cb994 | ||
|
|
e96dd00652 | ||
|
|
945879fa38 | ||
|
|
8f5e5bff1e | ||
|
|
f0e2272e04 | ||
|
|
93221b4535 | ||
|
|
3ffd88a84a | ||
|
|
ade7bd8745 | ||
|
|
4ec83fbad6 | ||
|
|
cd916b728b | ||
|
|
f4f76eb275 | ||
|
|
16f3520089 | ||
|
|
c591c91653 | ||
|
|
67192a2323 | ||
|
|
8ee044ea4a | ||
|
|
da14e024a8 |
@@ -1 +1 @@
|
||||
1.23.0
|
||||
1.24.2
|
||||
|
||||
@@ -11,6 +11,9 @@ import "tailscale.com/tailcfg"
|
||||
type WhoIsResponse struct {
|
||||
Node *tailcfg.Node
|
||||
UserProfile *tailcfg.UserProfile
|
||||
|
||||
// Caps are extra capabilities that the remote Node has to this node.
|
||||
Caps []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// FileTarget is a node to which files can be sent, and the PeerAPI
|
||||
|
||||
@@ -381,6 +381,21 @@ func CheckIPForwarding(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPrefs validates the provided preferences, without making any changes.
|
||||
//
|
||||
// The CLI uses this before a Start call to fail fast if the preferences won't
|
||||
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
|
||||
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
|
||||
// EditPrefs is not necessary.
|
||||
func CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
|
||||
pj, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, bytes.NewReader(pj))
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
|
||||
body, err := get200(ctx, "/localapi/v0/prefs")
|
||||
if err != nil {
|
||||
|
||||
@@ -117,10 +117,32 @@ header.
|
||||
|
||||
The `Tailscale-Tailnet` header can help you identify which tailnet the session
|
||||
is coming from. If you are using node sharing, this can help you make sure that
|
||||
you aren't giving administrative access to people outside your tailnet. You will
|
||||
need to be sure to check this in your application code. If you use OpenResty,
|
||||
you may be able to do more complicated access controls than you can with NGINX
|
||||
alone.
|
||||
you aren't giving administrative access to people outside your tailnet.
|
||||
|
||||
### Allow Requests From Only One Tailnet
|
||||
|
||||
If you want to prevent node sharing from allowing users to access a service, add
|
||||
the `Expected-Tailnet` header to your auth request:
|
||||
|
||||
```nginx
|
||||
location /auth {
|
||||
# ...
|
||||
proxy_set_header Expected-Tailnet "tailscale.com";
|
||||
}
|
||||
```
|
||||
|
||||
If a user from a different tailnet tries to use that service, this will return a
|
||||
generic "forbidden" error page:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head><title>403 Forbidden</title></head>
|
||||
<body>
|
||||
<center><h1>403 Forbidden</h1></center>
|
||||
<hr><center>nginx/1.18.0 (Ubuntu)</center>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
14
cmd/nginx-auth/deb/postinst.sh
Executable file
14
cmd/nginx-auth/deb/postinst.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
|
||||
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
if deb-systemd-helper --quiet was-enabled 'tailscale.nginx-auth.socket'; then
|
||||
deb-systemd-helper enable 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
else
|
||||
deb-systemd-helper update-state 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
fi
|
||||
|
||||
if systemctl is-active tailscale.nginx-auth.socket >/dev/null; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
deb-systemd-invoke restart 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
19
cmd/nginx-auth/deb/postrm.sh
Executable file
19
cmd/nginx-auth/deb/postrm.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ -d /run/systemd/system ] ; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -x "/usr/bin/deb-systemd-helper" ]; then
|
||||
if [ "$1" = "remove" ]; then
|
||||
deb-systemd-helper mask 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
deb-systemd-helper mask 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$1" = "purge" ]; then
|
||||
deb-systemd-helper purge 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
deb-systemd-helper purge 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
deb-systemd-helper unmask 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
8
cmd/nginx-auth/deb/prerm.sh
Executable file
8
cmd/nginx-auth/deb/prerm.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "remove" ]; then
|
||||
if [ -d /run/systemd/system ]; then
|
||||
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
|
||||
deb-systemd-invoke stop 'tailscale.nginx-auth.socket' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
@@ -4,20 +4,28 @@ set -e
|
||||
|
||||
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
|
||||
|
||||
mkpkg \
|
||||
--out tailscale-nginx-auth-0.1.0-amd64.deb \
|
||||
--name=tailscale-nginx-auth \
|
||||
--version=0.1.0 \
|
||||
--type=deb\
|
||||
--arch=amd64 \
|
||||
--description="Tailscale NGINX authentication protocol handler" \
|
||||
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service
|
||||
VERSION=0.1.1
|
||||
|
||||
mkpkg \
|
||||
--out tailscale-nginx-auth-0.1.0-amd64.rpm \
|
||||
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
|
||||
--name=tailscale-nginx-auth \
|
||||
--version=0.1.0 \
|
||||
--version=${VERSION} \
|
||||
--type=deb \
|
||||
--arch=amd64 \
|
||||
--postinst=deb/postinst.sh \
|
||||
--postrm=deb/postrm.sh \
|
||||
--prerm=deb/prerm.sh \
|
||||
--description="Tailscale NGINX authentication protocol handler" \
|
||||
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
|
||||
|
||||
mkpkg \
|
||||
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
|
||||
--name=tailscale-nginx-auth \
|
||||
--version=${VERSION} \
|
||||
--type=rpm \
|
||||
--arch=amd64 \
|
||||
--postinst=rpm/postinst.sh \
|
||||
--postrm=rpm/postrm.sh \
|
||||
--prerm=rpm/prerm.sh \
|
||||
--description="Tailscale NGINX authentication protocol handler" \
|
||||
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service
|
||||
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -75,6 +76,12 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
|
||||
return
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
|
||||
h.Set("Tailscale-User", info.UserProfile.LoginName)
|
||||
|
||||
0
cmd/nginx-auth/rpm/postinst.sh
Executable file
0
cmd/nginx-auth/rpm/postinst.sh
Executable file
9
cmd/nginx-auth/rpm/postrm.sh
Executable file
9
cmd/nginx-auth/rpm/postrm.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
# $1 == 0 for uninstallation.
|
||||
# $1 == 1 for removing old package during upgrade.
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || :
|
||||
if [ $1 -ge 1 ] ; then
|
||||
# Package upgrade, not uninstall
|
||||
systemctl stop tailscale.nginx-auth.service >/dev/null 2>&1 || :
|
||||
systemctl try-restart tailscale.nginx-auth.socket >/dev/null 2>&1 || :
|
||||
fi
|
||||
9
cmd/nginx-auth/rpm/prerm.sh
Executable file
9
cmd/nginx-auth/rpm/prerm.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
# $1 == 0 for uninstallation.
|
||||
# $1 == 1 for removing old package during upgrade.
|
||||
|
||||
if [ $1 -eq 0 ] ; then
|
||||
# Package removal, not upgrade
|
||||
systemctl --no-reload disable tailscale.nginx-auth.socket > /dev/null 2>&1 || :
|
||||
systemctl stop tailscale.nginx-auth.socket > /dev/null 2>&1 || :
|
||||
systemctl stop tailscale.nginx-auth.service > /dev/null 2>&1 || :
|
||||
fi
|
||||
@@ -86,11 +86,12 @@ func main() {
|
||||
for i := 0; i < 60; i++ {
|
||||
st, err := tailscale.Status(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("tailscale status: %v", st.BackendState)
|
||||
if st.BackendState == "Running" {
|
||||
break
|
||||
log.Printf("error retrieving tailscale status; retrying: %v", err)
|
||||
} else {
|
||||
log.Printf("tailscale status: %v", st.BackendState)
|
||||
if st.BackendState == "Running" {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
@@ -21,9 +22,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
@@ -106,6 +109,11 @@ var debugCmd = &ffcli.Command{
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "via",
|
||||
Exec: runVia,
|
||||
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -348,3 +356,46 @@ func runDaemonMetrics(ctx context.Context, args []string) error {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func runVia(ctx context.Context, args []string) error {
|
||||
switch len(args) {
|
||||
default:
|
||||
return errors.New("expect either <site-id> <v4-cidr> or <v6-route>")
|
||||
case 1:
|
||||
ipp, err := netaddr.ParseIPPrefix(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ipp.IP().Is6() {
|
||||
return errors.New("with one argument, expect an IPv6 CIDR")
|
||||
}
|
||||
if !tsaddr.TailscaleViaRange().Contains(ipp.IP()) {
|
||||
return errors.New("not a via route")
|
||||
}
|
||||
if ipp.Bits() < 96 {
|
||||
return errors.New("short length, want /96 or more")
|
||||
}
|
||||
v4 := tsaddr.UnmapVia(ipp.IP())
|
||||
a := ipp.IP().As16()
|
||||
siteID := binary.BigEndian.Uint32(a[8:12])
|
||||
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netaddr.IPPrefixFrom(v4, ipp.Bits()-96))
|
||||
case 2:
|
||||
siteID, err := strconv.ParseUint(args[0], 0, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid site-id %q; must be decimal or hex with 0x prefix", args[0])
|
||||
}
|
||||
if siteID > 0xff {
|
||||
return fmt.Errorf("site-id values over 255 are currently reserved")
|
||||
}
|
||||
ipp, err := netaddr.ParseIPPrefix(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
via, err := tsaddr.MapVia(uint32(siteID), ipp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -76,34 +75,52 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
argv := append([]string{
|
||||
ssh,
|
||||
argv := []string{ssh}
|
||||
|
||||
"-o", fmt.Sprintf("UserKnownHostsFile %s",
|
||||
shellescape.Quote(knownHostsFile),
|
||||
),
|
||||
"-o", fmt.Sprintf("ProxyCommand %s --socket=%s nc %%h %%p",
|
||||
shellescape.Quote(tailscaleBin),
|
||||
shellescape.Quote(rootArgs.socket),
|
||||
),
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
argv = append(argv, "-vvv")
|
||||
}
|
||||
argv = append(argv,
|
||||
// Only trust SSH hosts that we know about.
|
||||
"-o", fmt.Sprintf("UserKnownHostsFile %q", knownHostsFile),
|
||||
"-o", "UpdateHostKeys no",
|
||||
"-o", "StrictHostKeyChecking yes",
|
||||
)
|
||||
|
||||
// Explicitly rebuild the user@host argument rather than
|
||||
// passing it through. In general, the use of OpenSSH's ssh
|
||||
// binary is a crutch for now. We don't want to be
|
||||
// Hyrum-locked into passing through all OpenSSH flags to the
|
||||
// OpenSSH client forever. We try to make our flags and args
|
||||
// be compatible, but only a subset. The "tailscale ssh"
|
||||
// command should be a simple and portable one. If they want
|
||||
// to use a different one, we'll later be making stock ssh
|
||||
// work well by default too. (doing things like automatically
|
||||
// setting known_hosts, etc)
|
||||
username + "@" + hostForSSH,
|
||||
}, argRest...)
|
||||
// TODO(bradfitz): nc is currently broken on macOS:
|
||||
// https://github.com/tailscale/tailscale/issues/4529
|
||||
// So don't use it for now. MagicDNS is usually working on macOS anyway
|
||||
// and they're not in userspace mode, so 'nc' isn't very useful.
|
||||
if runtime.GOOS != "darwin" {
|
||||
argv = append(argv,
|
||||
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
|
||||
tailscaleBin,
|
||||
rootArgs.socket,
|
||||
))
|
||||
}
|
||||
|
||||
// Explicitly rebuild the user@host argument rather than
|
||||
// passing it through. In general, the use of OpenSSH's ssh
|
||||
// binary is a crutch for now. We don't want to be
|
||||
// Hyrum-locked into passing through all OpenSSH flags to the
|
||||
// OpenSSH client forever. We try to make our flags and args
|
||||
// be compatible, but only a subset. The "tailscale ssh"
|
||||
// command should be a simple and portable one. If they want
|
||||
// to use a different one, we'll later be making stock ssh
|
||||
// work well by default too. (doing things like automatically
|
||||
// setting known_hosts, etc)
|
||||
argv = append(argv, username+"@"+hostForSSH)
|
||||
|
||||
argv = append(argv, argRest...)
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
log.Printf("Running: %q, %q ...", ssh, argv)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Don't use syscall.Exec on Windows.
|
||||
cmd := exec.Command(ssh, argv[1:]...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
var ee *exec.ExitError
|
||||
@@ -114,9 +131,6 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
log.Printf("Running: %q, %q ...", ssh, argv)
|
||||
}
|
||||
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
@@ -53,7 +52,7 @@ down").
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset flag
|
||||
is also used. (The flags --authkey, --force-reauth, and --qr are not
|
||||
is also used. (The flags --auth-key, --force-reauth, and --qr are not
|
||||
considered settings that need to be re-specified when modifying
|
||||
settings.)
|
||||
`),
|
||||
@@ -100,9 +99,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
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")
|
||||
if envknob.UseWIPCode() || inTest() {
|
||||
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
}
|
||||
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
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\")")
|
||||
upf.StringVar(&upArgs.authKeyOrFile, "auth-key", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
@@ -595,6 +592,10 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tailscale.CheckPrefs(ctx, prefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authKey, err := upArgs.getAuthKey()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
github.com/alessio/shellescape from tailscale.com/cmd/tailscale/cli
|
||||
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
|
||||
|
||||
@@ -82,6 +82,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
LD github.com/kr/fs from github.com/pkg/sftp
|
||||
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
@@ -89,6 +90,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
|
||||
@@ -231,7 +234,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/wgengine/netstack
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
@@ -261,6 +264,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/netconv from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
@@ -298,7 +302,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
|
||||
12
cmd/tailscaled/ssh.go
Normal file
12
cmd/tailscaled/ssh.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package main
|
||||
|
||||
// Force registration of tailssh with LocalBackend.
|
||||
import _ "tailscale.com/ssh/tailssh"
|
||||
|
||||
@@ -332,6 +332,7 @@ func run() error {
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
dialer.Logf = logf
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
@@ -394,6 +395,7 @@ func run() error {
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
defer dialer.Close()
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
|
||||
@@ -34,12 +34,13 @@ import (
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
@@ -56,7 +57,8 @@ import (
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
dialer *tsdial.Dialer
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
@@ -105,6 +107,7 @@ type Options struct {
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
@@ -169,13 +172,12 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := netns.NewDialer(opts.Logf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
|
||||
tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
// Disable implicit gzip compression; the various
|
||||
// handlers (register, map, set-dns, etc) do their own
|
||||
@@ -201,6 +203,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
dialer: opts.Dialer,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
@@ -375,7 +378,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if err != nil {
|
||||
return regen, opt.URL, err
|
||||
}
|
||||
c.logf("control server key %s from %s", serverKey.ShortString(), c.serverURL)
|
||||
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
||||
|
||||
c.mu.Lock()
|
||||
c.serverKey = keys.LegacyPublicKey
|
||||
@@ -850,7 +853,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
|
||||
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
|
||||
metricMapResponsePings.Add(1)
|
||||
go answerPing(c.logf, c.httpc, pr)
|
||||
go answerPing(c.logf, c.httpc, pr, c.pinger)
|
||||
}
|
||||
if u := resp.PopBrowserURL; u != "" && u != sess.lastPopBrowserURL {
|
||||
sess.lastPopBrowserURL = u
|
||||
@@ -895,6 +898,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
c.logf("exiting process with status %v per controlplane", *code)
|
||||
os.Exit(*code)
|
||||
}
|
||||
if resp.Debug.DisableLogTail {
|
||||
logtail.Disable()
|
||||
}
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
@@ -1181,29 +1187,47 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) {
|
||||
if pr.URL == "" {
|
||||
logf("invalid PingRequest with no URL")
|
||||
return
|
||||
}
|
||||
if pr.Types == "" {
|
||||
answerHeadPing(logf, c, pr)
|
||||
return
|
||||
}
|
||||
for _, t := range strings.Split(pr.Types, ",") {
|
||||
switch t {
|
||||
case "TSMP", "disco":
|
||||
go doPingerPing(logf, c, pr, pinger, t)
|
||||
// TODO(tailscale/corp#754)
|
||||
// case "host":
|
||||
// case "peerapi":
|
||||
default:
|
||||
logf("unsupported ping request type: %q", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func answerHeadPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", pr.URL, nil)
|
||||
if err != nil {
|
||||
logf("http.NewRequestWithContext(%q): %v", pr.URL, err)
|
||||
logf("answerHeadPing: NewRequestWithContext: %v", err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerPing: sending ping to %v ...", pr.URL)
|
||||
logf("answerHeadPing: sending HEAD ping to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
logf("answerPing error: %v to %v (after %v)", err, pr.URL, d)
|
||||
logf("answerHeadPing error: %v to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("answerPing complete to %v (after %v)", pr.URL, d)
|
||||
logf("answerHeadPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,7 +1280,7 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL)
|
||||
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1376,35 +1400,28 @@ func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
|
||||
// with ping response data.
|
||||
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
|
||||
var err error
|
||||
if pr.URL == "" {
|
||||
return errors.New("invalid PingRequest with no URL")
|
||||
// doPingerPing sends a Ping to pr.IP using pinger, and sends an http request back to
|
||||
// pr.URL with ping response data.
|
||||
func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger, pingType string) {
|
||||
if pr.URL == "" || pr.IP.IsZero() || pinger == nil {
|
||||
logf("invalid ping request: missing url, ip or pinger")
|
||||
return
|
||||
}
|
||||
if pr.IP.IsZero() {
|
||||
return errors.New("PingRequest without IP")
|
||||
}
|
||||
if !strings.Contains(pr.Types, "TSMP") {
|
||||
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
|
||||
start := time.Now()
|
||||
pinger.Ping(pr.IP, pingType == "TSMP", func(res *ipnstate.PingResult) {
|
||||
// Currently does not check for error since we just return if it fails.
|
||||
err = postPingResult(now, logf, c, pr, res)
|
||||
postPingResult(start, logf, c, pr, res.ToPingResponse(pingType))
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
|
||||
if res.Err != "" {
|
||||
return errors.New(res.Err)
|
||||
}
|
||||
duration := time.Since(now)
|
||||
func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *tailcfg.PingResponse) error {
|
||||
duration := time.Since(start)
|
||||
if pr.Log {
|
||||
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
|
||||
if res.Err == "" {
|
||||
logf("ping to %v completed in %v. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration)
|
||||
} else {
|
||||
logf("ping to %v failed after %v: %v", pr.IP, duration, res.Err)
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
@@ -1414,20 +1431,20 @@ func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg
|
||||
return err
|
||||
}
|
||||
// Send the results of the Ping, back to control URL.
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewReader(jsonPingRes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
|
||||
}
|
||||
if pr.Log {
|
||||
logf("tsmpPing: sending ping results to %v ...", pr.URL)
|
||||
logf("postPingResult: sending ping results to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
|
||||
return fmt.Errorf("postPingResult error: %w to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
|
||||
logf("postPingResult complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -30,6 +31,7 @@ func TestNewDirect(t *testing.T) {
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: new(tsdial.Dialer),
|
||||
}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
@@ -106,6 +108,7 @@ func TestTsmpPing(t *testing.T) {
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: new(tsdial.Dialer),
|
||||
}
|
||||
|
||||
c, err := NewDirect(opts)
|
||||
@@ -113,7 +116,8 @@ func TestTsmpPing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pingRes := &ipnstate.PingResult{
|
||||
pingRes := &tailcfg.PingResponse{
|
||||
Type: "TSMP",
|
||||
IP: "123.456.7890",
|
||||
Err: "",
|
||||
NodeName: "testnode",
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
@@ -45,6 +47,7 @@ func (c *noiseConn) Close() error {
|
||||
// the ts2021 protocol.
|
||||
type noiseClient struct {
|
||||
*http.Client // HTTP client used to talk to tailcontrol
|
||||
dialer *tsdial.Dialer
|
||||
privKey key.MachinePrivate
|
||||
serverPubKey key.MachinePublic
|
||||
serverHost string // the host:port part of serverURL
|
||||
@@ -57,7 +60,7 @@ type noiseClient struct {
|
||||
|
||||
// newNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string) (*noiseClient, error) {
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,6 +77,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
|
||||
serverPubKey: serverPubKey,
|
||||
privKey: priKey,
|
||||
serverHost: host,
|
||||
dialer: dialer,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
@@ -137,9 +141,6 @@ func (nc *noiseClient) Close() error {
|
||||
func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
nc.mu.Lock()
|
||||
connID := nc.nextID
|
||||
if nc.connPool == nil {
|
||||
nc.connPool = make(map[int]*noiseConn)
|
||||
}
|
||||
nc.nextID++
|
||||
nc.mu.Unlock()
|
||||
|
||||
@@ -153,7 +154,7 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion))
|
||||
conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -161,6 +162,6 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
nc.mu.Lock()
|
||||
defer nc.mu.Unlock()
|
||||
ncc := &noiseConn{Conn: conn, id: connID, pool: nc}
|
||||
nc.connPool[ncc.id] = ncc
|
||||
mak.Set(&nc.connPool, ncc.id, ncc)
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
@@ -25,16 +25,15 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
@@ -65,13 +64,12 @@ const (
|
||||
//
|
||||
// The provided ctx is only used for the initial connection, until
|
||||
// Dial returns. It does not affect the connection once established.
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*controlbase.Conn, error) {
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &dialParams{
|
||||
ctx: ctx,
|
||||
host: host,
|
||||
httpPort: port,
|
||||
httpsPort: "443",
|
||||
@@ -79,12 +77,12 @@ func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, contr
|
||||
controlKey: controlKey,
|
||||
version: protocolVersion,
|
||||
proxyFunc: tshttpproxy.ProxyFromEnvironment,
|
||||
dialer: dialer,
|
||||
}
|
||||
return a.dial()
|
||||
return a.dial(ctx)
|
||||
}
|
||||
|
||||
type dialParams struct {
|
||||
ctx context.Context
|
||||
host string
|
||||
httpPort string
|
||||
httpsPort string
|
||||
@@ -92,65 +90,132 @@ type dialParams struct {
|
||||
controlKey key.MachinePublic
|
||||
version uint16
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
dialer dnscache.DialContextFunc
|
||||
|
||||
// For tests only
|
||||
insecureTLS bool
|
||||
insecureTLS bool
|
||||
testFallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func (a *dialParams) dial() (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
|
||||
// starting to try a.httpsPort.
|
||||
func (a *dialParams) httpsFallbackDelay() time.Duration {
|
||||
if v := a.testFallbackDelay; v != 0 {
|
||||
return v
|
||||
}
|
||||
return 500 * time.Millisecond
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
// Create one shared context used by both port 80 and port 443 dials.
|
||||
// If port 80 is still in flight when 443 returns, this deferred cancel
|
||||
// will stop the port 80 dial.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
|
||||
// respectively, in order to do the HTTP upgrade to a net.Conn over which
|
||||
// we'll speak Noise.
|
||||
u80 := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: net.JoinHostPort(a.host, a.httpPort),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
conn, httpErr := a.tryURL(u, init)
|
||||
if httpErr == nil {
|
||||
ret, err := cont(a.ctx, conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
u443 := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: net.JoinHostPort(a.host, a.httpsPort),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
|
||||
// Connecting over plain HTTP failed, assume it's an HTTP proxy
|
||||
// being difficult and see if we can get through over HTTPS.
|
||||
u.Scheme = "https"
|
||||
u.Host = net.JoinHostPort(a.host, a.httpsPort)
|
||||
init, cont, err = controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
type tryURLRes struct {
|
||||
u *url.URL // input (the URL conn+err are for/from)
|
||||
conn *controlbase.Conn // result (mutually exclusive with err)
|
||||
err error
|
||||
}
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
cbConn, err := a.dialURL(ctx, u)
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
case <-ctx.Done():
|
||||
if cbConn != nil {
|
||||
cbConn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the plaintext HTTP attempt first.
|
||||
go try(u80)
|
||||
|
||||
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
|
||||
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
|
||||
try443Timer := time.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
|
||||
defer try443Timer.Stop()
|
||||
|
||||
var err80, err443 error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("connection attempts aborted by context: %w", ctx.Err())
|
||||
case res := <-ch:
|
||||
if res.err == nil {
|
||||
return res.conn, nil
|
||||
}
|
||||
switch res.u {
|
||||
case u80:
|
||||
// Connecting over plain HTTP failed; assume it's an HTTP proxy
|
||||
// being difficult and see if we can get through over HTTPS.
|
||||
err80 = res.err
|
||||
// Stop the fallback timer and run it immediately. We don't use
|
||||
// Timer.Reset(0) here because on AfterFuncs, that can run it
|
||||
// again.
|
||||
if try443Timer.Stop() {
|
||||
go try(u443)
|
||||
} // else we lost the race and it started already which is what we want
|
||||
case u443:
|
||||
err443 = res.err
|
||||
default:
|
||||
panic("invalid")
|
||||
}
|
||||
if err80 != nil && err443 != nil {
|
||||
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", err80, err443)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dialURL attempts to connect to the given URL.
|
||||
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, tlsErr := a.tryURL(u, init)
|
||||
if tlsErr == nil {
|
||||
ret, err := cont(a.ctx, conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return cbConn, nil
|
||||
}
|
||||
|
||||
func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
|
||||
dns := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
dialer := netns.NewDialer(log.Printf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
defer tr.CloseIdleConnections()
|
||||
tr.Proxy = a.proxyFunc
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dns)
|
||||
tr.DialContext = dnscache.Dialer(a.dialer, dns)
|
||||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{}
|
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
||||
@@ -159,7 +224,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
tr.TLSClientConfig.VerifyConnection = nil
|
||||
}
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dns, tr.TLSClientConfig)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
|
||||
tr.DisableCompression = true
|
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
@@ -189,7 +254,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
connCh <- info.Conn
|
||||
},
|
||||
}
|
||||
ctx := httptrace.WithClientTrace(a.ctx, &trace)
|
||||
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: u,
|
||||
|
||||
@@ -17,22 +17,36 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
type httpTestParam struct {
|
||||
name string
|
||||
proxy proxy
|
||||
|
||||
// makeHTTPHangAfterUpgrade makes the HTTP response hang after sending a
|
||||
// 101 switching protocols.
|
||||
makeHTTPHangAfterUpgrade bool
|
||||
}
|
||||
|
||||
func TestControlHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
proxy proxy
|
||||
}{
|
||||
tests := []httpTestParam{
|
||||
// direct connection
|
||||
{
|
||||
name: "no_proxy",
|
||||
proxy: nil,
|
||||
},
|
||||
// direct connection but port 80 is MITM'ed and broken
|
||||
{
|
||||
name: "port80_broken_mitm",
|
||||
proxy: nil,
|
||||
makeHTTPHangAfterUpgrade: true,
|
||||
},
|
||||
// SOCKS5
|
||||
{
|
||||
name: "socks5",
|
||||
@@ -96,12 +110,13 @@ func TestControlHTTP(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testControlHTTP(t, test.proxy)
|
||||
testControlHTTP(t, test)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
func testControlHTTP(t *testing.T, param httpTestParam) {
|
||||
proxy := param.proxy
|
||||
client, server := key.NewMachine(), key.NewMachine()
|
||||
|
||||
const testProtocolVersion = 1
|
||||
@@ -132,7 +147,11 @@ func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
t.Fatalf("HTTPS listen: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Handler: handler}
|
||||
var httpHandler http.Handler = handler
|
||||
if param.makeHTTPHangAfterUpgrade {
|
||||
httpHandler = http.HandlerFunc(brokenMITMHandler)
|
||||
}
|
||||
httpServer := &http.Server{Handler: httpHandler}
|
||||
go httpServer.Serve(httpLn)
|
||||
defer httpServer.Close()
|
||||
|
||||
@@ -143,18 +162,24 @@ func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
go httpsServer.ServeTLS(httpsLn, "", "")
|
||||
defer httpsServer.Close()
|
||||
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
ctx := context.Background()
|
||||
const debugTimeout = false
|
||||
if debugTimeout {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
a := dialParams{
|
||||
ctx: context.Background(), //ctx,
|
||||
host: "localhost",
|
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
machineKey: client,
|
||||
controlKey: server.Public(),
|
||||
version: testProtocolVersion,
|
||||
insecureTLS: true,
|
||||
host: "localhost",
|
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
machineKey: client,
|
||||
controlKey: server.Public(),
|
||||
version: testProtocolVersion,
|
||||
insecureTLS: true,
|
||||
dialer: new(tsdial.Dialer).SystemDial,
|
||||
testFallbackDelay: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
if proxy != nil {
|
||||
@@ -173,7 +198,7 @@ func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := a.dial()
|
||||
conn, err := a.dial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
@@ -215,6 +240,7 @@ type proxy interface {
|
||||
|
||||
type socksProxy struct {
|
||||
sync.Mutex
|
||||
closed bool
|
||||
proxy socks5.Server
|
||||
ln net.Listener
|
||||
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
|
||||
@@ -230,7 +256,14 @@ func (s *socksProxy) Start(t *testing.T) (url string) {
|
||||
}
|
||||
s.ln = ln
|
||||
s.clientConnAddrs = map[string]bool{}
|
||||
s.proxy.Logf = t.Logf
|
||||
s.proxy.Logf = func(format string, a ...any) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
t.Logf(format, a...)
|
||||
}
|
||||
s.proxy.Dialer = s.dialAndRecord
|
||||
go s.proxy.Serve(ln)
|
||||
return fmt.Sprintf("socks5://%s", ln.Addr().String())
|
||||
@@ -239,6 +272,10 @@ func (s *socksProxy) Start(t *testing.T) (url string) {
|
||||
func (s *socksProxy) Close() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.ln.Close()
|
||||
}
|
||||
|
||||
@@ -398,3 +435,11 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
|
||||
func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Upgrade", upgradeHeaderValue)
|
||||
w.Header().Set("Connection", "upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}
|
||||
|
||||
@@ -538,12 +538,17 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
return tls.Client(nc, tlsConf)
|
||||
}
|
||||
|
||||
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, err error) {
|
||||
// DialRegionTLS returns a TLS connection to a DERP node in the given region.
|
||||
//
|
||||
// DERP nodes for a region are tried in sequence according to their order
|
||||
// in the DERP map. TLS is initiated on the first node where a socket is
|
||||
// established.
|
||||
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
|
||||
tcpConn, node, err := c.dialRegion(ctx, reg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
done := make(chan bool) // unbufferd
|
||||
done := make(chan bool) // unbuffered
|
||||
defer close(done)
|
||||
|
||||
tlsConn = c.tlsClient(tcpConn, node)
|
||||
@@ -556,13 +561,13 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
|
||||
}()
|
||||
err = tlsConn.Handshake()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
select {
|
||||
case done <- true:
|
||||
return tlsConn, tcpConn, nil
|
||||
return tlsConn, tcpConn, node, nil
|
||||
case <-ctx.Done():
|
||||
return nil, nil, ctx.Err()
|
||||
return nil, nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,3 +149,9 @@ func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
|
||||
// if already enabled and any attempt to re-enable it will result in
|
||||
// an error.
|
||||
func CanSSHD() bool { return !Bool("TS_DISABLE_SSH_SERVER") }
|
||||
|
||||
// SSHPolicyFile returns the path, if any, to the SSHPolicy JSON file for development.
|
||||
func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
|
||||
|
||||
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
|
||||
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }
|
||||
|
||||
11
go.mod
11
go.mod
@@ -5,7 +5,6 @@ go 1.18
|
||||
require (
|
||||
filippo.io/mkcert v1.4.3
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alessio/shellescape v1.4.1
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2
|
||||
@@ -14,6 +13,7 @@ require (
|
||||
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/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/frankban/quicktest v1.14.0
|
||||
@@ -39,7 +39,7 @@ require (
|
||||
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
@@ -48,10 +48,10 @@ require (
|
||||
github.com/u-root/u-root v0.8.0
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1
|
||||
@@ -105,7 +105,6 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/charithe/durationcheck v0.0.9 // indirect
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
@@ -252,7 +251,7 @@ require (
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
|
||||
17
go.sum
17
go.sum
@@ -104,8 +104,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
|
||||
@@ -1067,8 +1065,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf h1:+DSoknr7gaiW2LlViX6+ko8TBdxTLkvOBbIWQtYyMaE=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 h1:f7nwzdAHTUUOJjHZuDvLz9CEAlUM228amCRvwzlPvsA=
|
||||
@@ -1227,8 +1225,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1478,8 +1476,8 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
@@ -1617,8 +1615,9 @@ golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1/go.mod h1:Uh6Zz+xoGYZom
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
|
||||
@@ -1 +1 @@
|
||||
5ce3ec4d89c72f2a2b6f6f5089c950d7a6a33530
|
||||
710a0d861098c07540ad073bb73a42ce81bf54a8
|
||||
|
||||
@@ -17,11 +17,14 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var started = time.Now()
|
||||
|
||||
// New returns a partially populated Hostinfo for the current host.
|
||||
func New() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
@@ -31,6 +34,7 @@ func New() *tailcfg.Hostinfo {
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: GetOSVersion(),
|
||||
Desktop: desktop(),
|
||||
Package: packageTypeCached(),
|
||||
GoArch: runtime.GOARCH,
|
||||
DeviceModel: deviceModel(),
|
||||
@@ -97,6 +101,7 @@ func GetEnvType() EnvType {
|
||||
var (
|
||||
deviceModelAtomic atomic.Value // of string
|
||||
osVersionAtomic atomic.Value // of string
|
||||
desktopAtomic atomic.Value // of opt.Bool
|
||||
packagingType atomic.Value // of string
|
||||
)
|
||||
|
||||
@@ -117,6 +122,31 @@ func deviceModel() string {
|
||||
return s
|
||||
}
|
||||
|
||||
func desktop() (ret opt.Bool) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return opt.Bool("")
|
||||
}
|
||||
if v := desktopAtomic.Load(); v != nil {
|
||||
v, _ := v.(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
seenDesktop := false
|
||||
lineread.File("/proc/net/unix", func(line []byte) error {
|
||||
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
|
||||
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
|
||||
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
|
||||
return nil
|
||||
})
|
||||
ret.Set(seenDesktop)
|
||||
|
||||
// Only cache after a minute - compositors might not have started yet.
|
||||
if time.Since(started) > time.Minute {
|
||||
desktopAtomic.Store(ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func getEnvType() EnvType {
|
||||
if inKnative() {
|
||||
return KNative
|
||||
|
||||
@@ -259,10 +259,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.8.8:53"},
|
||||
{Addr: "8.8.8.8"},
|
||||
},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4:53"}},
|
||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -283,7 +283,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4:53"},
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -73,6 +73,25 @@ func getControlDebugFlags() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHServer is the interface of the conditionally linked ssh/tailssh.server.
|
||||
type SSHServer interface {
|
||||
HandleSSHConn(net.Conn) error
|
||||
|
||||
// OnPolicyChange is called when the SSH access policy changes,
|
||||
// so that existing sessions can be re-evaluated for validity
|
||||
// and closed if they'd no longer be accepted.
|
||||
OnPolicyChange()
|
||||
}
|
||||
|
||||
type newSSHServerFunc func(logger.Logf, *LocalBackend) (SSHServer, error)
|
||||
|
||||
var newSSHServer newSSHServerFunc // or nil
|
||||
|
||||
// RegisterNewSSHServer lets the conditionally linked ssh/tailssh package register itself.
|
||||
func RegisterNewSSHServer(fn newSSHServerFunc) {
|
||||
newSSHServer = fn
|
||||
}
|
||||
|
||||
// LocalBackend is the glue between the major pieces of the Tailscale
|
||||
// network software: the cloud control plane (via controlclient), the
|
||||
// network data plane (via wgengine), and the user-facing UIs and CLIs
|
||||
@@ -103,14 +122,14 @@ type LocalBackend struct {
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
sshAtomicBool syncs.AtomicBool
|
||||
|
||||
filterHash deephash.Sum
|
||||
sshServer SSHServer // or nil
|
||||
|
||||
filterAtomic atomic.Value // of *filter.Filter
|
||||
containsViaIPFuncAtomic atomic.Value // of func(netaddr.IP) bool
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
filterHash deephash.Sum
|
||||
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
|
||||
ccGen clientGen // function for producing controlclient; lazily populated
|
||||
notify func(ipn.Notify)
|
||||
@@ -205,6 +224,12 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
loginFlags: loginFlags,
|
||||
}
|
||||
if newSSHServer != nil {
|
||||
b.sshServer, err = newSSHServer(logf, b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newSSHServer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
|
||||
@@ -295,7 +320,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
|
||||
// If the local network configuration has changed, our filter may
|
||||
// need updating to tweak default routes.
|
||||
b.updateFilter(b.netMap, b.prefs)
|
||||
b.updateFilterLocked(b.netMap, b.prefs)
|
||||
|
||||
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
|
||||
want := len(b.netMap.Addresses)
|
||||
@@ -319,6 +344,7 @@ func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
|
||||
func (b *LocalBackend) Shutdown() {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
b.closePeerAPIListenersLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
b.unregisterLinkMon()
|
||||
@@ -505,6 +531,30 @@ func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.Use
|
||||
return n, u, true
|
||||
}
|
||||
|
||||
// PeerCaps returns the capabilities that remote src IP has to
|
||||
// ths current node.
|
||||
func (b *LocalBackend) PeerCaps(src netaddr.IP) []string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.netMap == nil {
|
||||
return nil
|
||||
}
|
||||
filt, ok := b.filterAtomic.Load().(*filter.Filter)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, a := range b.netMap.Addresses {
|
||||
if !a.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
dstIP := a.IP()
|
||||
if dstIP.BitLen() == src.BitLen() {
|
||||
return filt.AppendCaps(nil, src, a.IP())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDecompressor sets a decompression function, which must be a zstd
|
||||
// reader.
|
||||
//
|
||||
@@ -616,7 +666,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if prefsChanged {
|
||||
prefs = b.prefs.Clone()
|
||||
}
|
||||
|
||||
if st.NetMap != nil {
|
||||
b.updateFilterLocked(st.NetMap, prefs)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
// Now complete the lock-free parts of what we started while locked.
|
||||
@@ -638,7 +690,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
}
|
||||
|
||||
b.updateFilter(st.NetMap, prefs)
|
||||
b.e.SetNetworkMap(st.NetMap)
|
||||
b.e.SetDERPMap(st.NetMap.DERPMap)
|
||||
|
||||
@@ -923,10 +974,9 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
b.setNetMapLocked(nil)
|
||||
persistv := b.prefs.Persist
|
||||
b.updateFilterLocked(nil, nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
b.updateFilter(nil, nil)
|
||||
|
||||
if b.portpoll != nil {
|
||||
b.portpollOnce.Do(func() {
|
||||
go b.portpoll.Run(b.ctx)
|
||||
@@ -984,6 +1034,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b.e,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
Dialer: b.Dialer(),
|
||||
|
||||
// Don't warn about broken Linux IP forwarding when
|
||||
// netstack is being used.
|
||||
@@ -1024,9 +1075,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateFilter updates the packet filter in wgengine based on the
|
||||
// updateFilterLocked updates the packet filter in wgengine based on the
|
||||
// given netMap and user preferences.
|
||||
func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
|
||||
// NOTE(danderson): keep change detection as the first thing in
|
||||
// this function. Don't try to optimize by returning early, more
|
||||
// likely than not you'll just end up breaking the change
|
||||
@@ -1082,8 +1135,12 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
}
|
||||
localNets, _ := localNetsB.IPSet()
|
||||
logNets, _ := logNetsB.IPSet()
|
||||
var sshPol tailcfg.SSHPolicy
|
||||
if haveNetmap && netMap.SSHPolicy != nil {
|
||||
sshPol = *netMap.SSHPolicy
|
||||
}
|
||||
|
||||
changed := deephash.Update(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
changed := deephash.Update(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp, sshPol)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
@@ -1102,6 +1159,10 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
|
||||
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
|
||||
if b.sshServer != nil {
|
||||
go b.sshServer.OnPolicyChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setFilter(f *filter.Filter) {
|
||||
@@ -1720,11 +1781,53 @@ func (b *LocalBackend) SetCurrentUserID(uid string) {
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.checkPrefsLocked(p)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
if p.Hostname == "badhostname.tailscale." {
|
||||
// Keep this one just for testing.
|
||||
return errors.New("bad hostname [test]")
|
||||
}
|
||||
if p.RunSSH {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
}
|
||||
if b.netMap != nil && b.netMap.SSHPolicy == nil &&
|
||||
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
b.mu.Lock()
|
||||
p0 := b.prefs.Clone()
|
||||
p1 := b.prefs.Clone()
|
||||
p1.ApplyEdits(mp)
|
||||
if err := b.checkPrefsLocked(p1); err != nil {
|
||||
b.mu.Unlock()
|
||||
b.logf("EditPrefs check error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if p1.RunSSH && !canSSH {
|
||||
b.mu.Unlock()
|
||||
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
|
||||
@@ -1780,6 +1883,12 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
userID := b.userID
|
||||
cc := b.cc
|
||||
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
if caller == "SetPrefs" {
|
||||
b.logf("SetPrefs: %v", newp.Pretty())
|
||||
}
|
||||
b.updateFilterLocked(netMap, newp)
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
if stateKey != "" {
|
||||
@@ -1789,10 +1898,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
}
|
||||
b.writeServerModeStartState(userID, newp)
|
||||
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
if caller == "SetPrefs" {
|
||||
b.logf("SetPrefs: %v", newp.Pretty())
|
||||
}
|
||||
if netMap != nil {
|
||||
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
|
||||
if newp.Persist == nil {
|
||||
@@ -1812,8 +1917,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
b.doSetHostinfoFilterServices(newHi)
|
||||
}
|
||||
|
||||
b.updateFilter(netMap, newp)
|
||||
|
||||
if netMap != nil {
|
||||
b.e.SetDERPMap(netMap.DERPMap)
|
||||
}
|
||||
@@ -2110,7 +2213,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
|
||||
addDefault := func(resolvers []dnstype.Resolver) {
|
||||
for _, r := range resolvers {
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2139,7 +2242,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
dcfg.Routes[fqdn] = make([]dnstype.Resolver, 0, len(resolvers))
|
||||
|
||||
for _, r := range resolvers {
|
||||
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
|
||||
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2170,16 +2273,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
return dcfg
|
||||
}
|
||||
|
||||
func normalizeResolver(cfg dnstype.Resolver) dnstype.Resolver {
|
||||
if ip, err := netaddr.ParseIP(cfg.Addr); err == nil {
|
||||
// Add 53 here for bare IPs for consistency with previous data type.
|
||||
return dnstype.Resolver{
|
||||
Addr: netaddr.IPPortFrom(ip, 53).String(),
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SetVarRoot sets the root directory of Tailscale's writable
|
||||
// storage area . (e.g. "/var/lib/tailscale")
|
||||
//
|
||||
@@ -3225,3 +3318,10 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
return cc.DoNoiseRequest(req)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleSSHConn(c net.Conn) error {
|
||||
if b.sshServer == nil {
|
||||
return errors.New("no SSH server")
|
||||
}
|
||||
return b.sshServer.HandleSSHConn(c)
|
||||
}
|
||||
|
||||
@@ -386,6 +386,14 @@ func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64,
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
|
||||
// Android for whatever reason often has problems creating the peerapi listener.
|
||||
// But since we started intercepting it with netstack, it's not even important that
|
||||
// we have a real kernel-level listener. So just create a dummy listener on Android
|
||||
// and let netstack intercept it.
|
||||
if runtime.GOOS == "android" {
|
||||
return newFakePeerAPIListener(ip), nil
|
||||
}
|
||||
|
||||
ipStr := ip.String()
|
||||
|
||||
var lc net.ListenConfig
|
||||
@@ -428,8 +436,15 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
|
||||
return ln, nil
|
||||
}
|
||||
}
|
||||
// Fall back to random ephemeral port.
|
||||
return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
|
||||
// Fall back to some random ephemeral port.
|
||||
ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
|
||||
|
||||
// And if we're on a platform with netstack (anything but iOS), then just fallback to netstack.
|
||||
if err != nil && runtime.GOOS != "ios" {
|
||||
s.b.logf("peerapi: failed to do peerAPI listen, harmless (netstack available) but error was: %v", err)
|
||||
return newFakePeerAPIListener(ip), nil
|
||||
}
|
||||
return ln, err
|
||||
}
|
||||
|
||||
type peerAPIListener struct {
|
||||
@@ -548,6 +563,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/dnsfwd":
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
case "/v0/interfaces":
|
||||
h.handleServeInterfaces(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
@@ -562,6 +580,40 @@ This is my Tailscale device. Your device is %v.
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
i, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
dr, err := interfaces.DefaultRoute()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintln(w, "<h1>Interfaces</h1>")
|
||||
fmt.Fprintf(w, "<h3>Default route is %q(%d)</h3>\n", dr.InterfaceName, dr.InterfaceIndex)
|
||||
|
||||
fmt.Fprintln(w, "<table>")
|
||||
fmt.Fprint(w, "<tr>")
|
||||
for _, v := range []any{"Index", "Name", "MTU", "Flags", "Addrs"} {
|
||||
fmt.Fprintf(w, "<th>%v</th> ", v)
|
||||
}
|
||||
fmt.Fprint(w, "</tr>\n")
|
||||
i.ForeachInterface(func(iface interfaces.Interface, ipps []netaddr.IPPrefix) {
|
||||
fmt.Fprint(w, "<tr>")
|
||||
for _, v := range []any{iface.Index, iface.Name, iface.MTU, iface.Flags, ipps} {
|
||||
fmt.Fprintf(w, "<td>%v</td> ", v)
|
||||
}
|
||||
fmt.Fprint(w, "</tr>\n")
|
||||
})
|
||||
fmt.Fprintln(w, "</table>")
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
@@ -620,9 +672,29 @@ func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
}
|
||||
}
|
||||
|
||||
// canPutFile reports whether h can put a file ("Taildrop") to this node.
|
||||
func (h *peerAPIHandler) canPutFile() bool {
|
||||
return h.isSelf || h.peerHasCap(tailcfg.CapabilityFileSharingSend)
|
||||
}
|
||||
|
||||
// canDebug reports whether h can debug this node (goroutines, metrics,
|
||||
// magicsock internal state, etc).
|
||||
func (h *peerAPIHandler) canDebug() bool {
|
||||
return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerHasCap(wantCap string) bool {
|
||||
for _, hasCap := range h.ps.b.PeerCaps(h.remoteAddr.IP()) {
|
||||
if hasCap == wantCap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
if !h.canPutFile() {
|
||||
http.Error(w, "Taildrop access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.ps.b.hasCapFileSharing() {
|
||||
@@ -741,8 +813,8 @@ func approxSize(n int64) string {
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var buf []byte
|
||||
@@ -757,8 +829,8 @@ func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var data struct {
|
||||
@@ -777,8 +849,8 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
eng := h.ps.b.e
|
||||
@@ -792,8 +864,8 @@ func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
@@ -1027,3 +1099,49 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
||||
w.Write(j)
|
||||
return nil
|
||||
}
|
||||
|
||||
// newFakePeerAPIListener creates a new net.Listener that acts like
|
||||
// it's listening on the provided IP address and on TCP port 1.
|
||||
//
|
||||
// See docs on fakePeerAPIListener.
|
||||
func newFakePeerAPIListener(ip netaddr.IP) net.Listener {
|
||||
return &fakePeerAPIListener{
|
||||
addr: netaddr.IPPortFrom(ip, 1).TCPAddr(),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// fakePeerAPIListener is a net.Listener that has an Addr method returning a TCPAddr
|
||||
// for a given IP on port 1 (arbitrary) and can be Closed, but otherwise Accept
|
||||
// just blocks forever until closed. The purpose of this is to let the rest
|
||||
// of the LocalBackend/PeerAPI code run and think it's talking to the kernel,
|
||||
// even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
|
||||
// or we lack permission to listen on a port. It's okay to not actually listen via
|
||||
// the kernel because on almost all platforms (except iOS as of 2022-04-20) we
|
||||
// also intercept netstack TCP requests in to our peerapi port and hand it over
|
||||
// directly to peerapi, without involving the kernel. So this doesn't need to be
|
||||
// real. But the port number we return (1, in this case) is the port number we advertise
|
||||
// to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's
|
||||
// using it, it doesn't matter, as we intercept it first in netstack and the kernel
|
||||
// never notices.
|
||||
//
|
||||
// Eventually we'll remove this code and do this on all platforms, when iOS also uses
|
||||
// netstack.
|
||||
type fakePeerAPIListener struct {
|
||||
addr net.Addr
|
||||
|
||||
closeOnce sync.Once
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (fl *fakePeerAPIListener) Close() error {
|
||||
fl.closeOnce.Do(func() { close(fl.closed) })
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fl *fakePeerAPIListener) Accept() (net.Conn, error) {
|
||||
<-fl.closed
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
func (fl *fakePeerAPIListener) Addr() net.Addr { return fl.addr }
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("not owner"),
|
||||
bodyContains("Taildrop access denied"),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -480,6 +480,8 @@ func osEmoji(os string) string {
|
||||
|
||||
// PingResult contains response information for the "tailscale ping" subcommand,
|
||||
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
|
||||
// See tailcfg.PingResponse for a related response that is sent back to control
|
||||
// for remote diagnostic pings.
|
||||
type PingResult struct {
|
||||
IP string // ping destination
|
||||
NodeIP string // Tailscale IP of node handling IP (different for subnet routers)
|
||||
@@ -513,6 +515,22 @@ type PingResult struct {
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
func (pr *PingResult) ToPingResponse(pingType string) *tailcfg.PingResponse {
|
||||
return &tailcfg.PingResponse{
|
||||
Type: pingType,
|
||||
IP: pr.IP,
|
||||
NodeIP: pr.NodeIP,
|
||||
NodeName: pr.NodeName,
|
||||
Err: pr.Err,
|
||||
LatencySeconds: pr.LatencySeconds,
|
||||
Endpoint: pr.Endpoint,
|
||||
DERPRegionID: pr.DERPRegionID,
|
||||
DERPRegionCode: pr.DERPRegionCode,
|
||||
PeerAPIPort: pr.PeerAPIPort,
|
||||
IsLocalIP: pr.IsLocalIP,
|
||||
}
|
||||
}
|
||||
|
||||
func SortPeers(peers []*PeerStatus) {
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveLogout(w, r)
|
||||
case "/localapi/v0/prefs":
|
||||
h.servePrefs(w, r)
|
||||
case "/localapi/v0/check-prefs":
|
||||
h.serveCheckPrefs(w, r)
|
||||
case "/localapi/v0/check-ip-forwarding":
|
||||
h.serveCheckIPForwarding(w, r)
|
||||
case "/localapi/v0/bugreport":
|
||||
@@ -223,6 +225,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
res := &apitype.WhoIsResponse{
|
||||
Node: n,
|
||||
UserProfile: &u,
|
||||
Caps: b.PeerCaps(ipp.IP()),
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
@@ -375,7 +378,9 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
prefs, err = h.b.EditPrefs(mp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(resJSON{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
case "GET", "HEAD":
|
||||
@@ -390,6 +395,33 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
e.Encode(prefs)
|
||||
}
|
||||
|
||||
type resJSON struct {
|
||||
Error string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "checkprefs access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
p := new(ipn.Prefs)
|
||||
if err := json.NewDecoder(r.Body).Decode(p); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
err := h.b.CheckPrefs(p)
|
||||
var res resJSON
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
|
||||
@@ -428,9 +428,14 @@ func NewPrefs() *Prefs {
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
// If not configured, DefaultControlURL is returned instead.
|
||||
//
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
// the default, then DefaultControlURL is returned instead.
|
||||
func (p *Prefs) ControlURLOrDefault() string {
|
||||
if p.ControlURL != "" {
|
||||
if p.ControlURL != DefaultControlURL && IsLoginServerSynonym(p.ControlURL) {
|
||||
return DefaultControlURL
|
||||
}
|
||||
return p.ControlURL
|
||||
}
|
||||
return DefaultControlURL
|
||||
|
||||
@@ -810,3 +810,18 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlURLOrDefault(t *testing.T) {
|
||||
var p Prefs
|
||||
if got, want := p.ControlURLOrDefault(), DefaultControlURL; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
p.ControlURL = "http://foo.bar"
|
||||
if got, want := p.ControlURLOrDefault(), "http://foo.bar"; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
p.ControlURL = "https://login.tailscale.com"
|
||||
if got, want := p.ControlURLOrDefault(), DefaultControlURL; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// Provider returns a StateStore for the provided path.
|
||||
@@ -82,10 +83,7 @@ func Register(prefix string, fn Provider) {
|
||||
if _, ok := knownStores[prefix]; ok {
|
||||
panic(fmt.Sprintf("%q already registered", prefix))
|
||||
}
|
||||
if knownStores == nil {
|
||||
knownStores = make(map[string]Provider)
|
||||
}
|
||||
knownStores[prefix] = fn
|
||||
mak.Set(&knownStores, prefix, fn)
|
||||
}
|
||||
|
||||
// TryWindowsAppDataMigration attempts to copy the Windows state file
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/syncs"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
@@ -412,7 +413,18 @@ func (l *Logger) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// logtailDisabled is whether logtail uploads to logcatcher are disabled.
|
||||
var logtailDisabled syncs.AtomicBool
|
||||
|
||||
// Disable disables logtail uploads for the lifetime of the process.
|
||||
func Disable() {
|
||||
logtailDisabled.Set(true)
|
||||
}
|
||||
|
||||
func (l *Logger) send(jsonBlob []byte) (int, error) {
|
||||
if logtailDisabled.Get() {
|
||||
return len(jsonBlob), nil
|
||||
}
|
||||
n, err := l.buffer.Write(jsonBlob)
|
||||
if l.drainLogs == nil {
|
||||
select {
|
||||
|
||||
@@ -84,10 +84,7 @@ func (c Config) hasDefaultIPResolversOnly() bool {
|
||||
return false
|
||||
}
|
||||
for _, r := range c.DefaultResolvers {
|
||||
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
|
||||
continue
|
||||
}
|
||||
if _, err := netaddr.ParseIP(r.Addr); err != nil {
|
||||
if ipp, ok := r.IPPort(); !ok || ipp.Port() != 53 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
}
|
||||
var defaultRoutes []dnstype.Resolver
|
||||
for _, ip := range bcfg.Nameservers {
|
||||
defaultRoutes = append(defaultRoutes, dnstype.ResolverFromIP(ip))
|
||||
defaultRoutes = append(defaultRoutes, dnstype.Resolver{Addr: ip.String()})
|
||||
}
|
||||
rcfg.Routes["."] = defaultRoutes
|
||||
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
|
||||
@@ -188,23 +188,13 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
// DoH or custom-port entries with something like hasDefaultIPResolversOnly.
|
||||
func toIPsOnly(resolvers []dnstype.Resolver) (ret []netaddr.IP) {
|
||||
for _, r := range resolvers {
|
||||
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
|
||||
if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
|
||||
ret = append(ret, ipp.IP())
|
||||
} else if ip, err := netaddr.ParseIP(r.Addr); err == nil {
|
||||
ret = append(ret, ip)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func toIPPorts(ips []netaddr.IP) (ret []netaddr.IPPort) {
|
||||
ret = make([]netaddr.IPPort, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
ret = append(ret, netaddr.IPPortFrom(ip, 53))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *Manager) EnqueuePacket(bs []byte, proto ipproto.Proto, from, to netaddr.IPPort) error {
|
||||
return m.resolver.EnqueuePacket(bs, proto, from, to)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "corp",
|
||||
in: Config{
|
||||
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
|
||||
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
os: OSConfig{
|
||||
@@ -107,7 +107,7 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "corp-split",
|
||||
in: Config{
|
||||
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
|
||||
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
split: true,
|
||||
@@ -119,7 +119,7 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "corp-magic",
|
||||
in: Config{
|
||||
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
|
||||
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
Routes: upstreams("ts.com", ""),
|
||||
Hosts: hosts(
|
||||
@@ -131,7 +131,7 @@ func TestManager(t *testing.T) {
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
|
||||
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
@@ -141,7 +141,7 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "corp-magic-split",
|
||||
in: Config{
|
||||
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
|
||||
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
Routes: upstreams("ts.com", ""),
|
||||
Hosts: hosts(
|
||||
@@ -154,7 +154,7 @@ func TestManager(t *testing.T) {
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
|
||||
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
@@ -164,8 +164,8 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "corp-routes",
|
||||
in: Config{
|
||||
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53"),
|
||||
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
os: OSConfig{
|
||||
@@ -174,15 +174,15 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(
|
||||
".", "1.1.1.1:53", "9.9.9.9:53",
|
||||
"corp.com.", "2.2.2.2:53"),
|
||||
".", "1.1.1.1", "9.9.9.9",
|
||||
"corp.com.", "2.2.2.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "corp-routes-split",
|
||||
in: Config{
|
||||
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53"),
|
||||
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
split: true,
|
||||
@@ -192,14 +192,14 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(
|
||||
".", "1.1.1.1:53", "9.9.9.9:53",
|
||||
"corp.com.", "2.2.2.2:53"),
|
||||
".", "1.1.1.1", "9.9.9.9",
|
||||
"corp.com.", "2.2.2.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "routes",
|
||||
in: Config{
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
bs: OSConfig{
|
||||
@@ -212,14 +212,14 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(
|
||||
".", "8.8.8.8:53",
|
||||
"corp.com.", "2.2.2.2:53"),
|
||||
".", "8.8.8.8",
|
||||
"corp.com.", "2.2.2.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "routes-split",
|
||||
in: Config{
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53"),
|
||||
Routes: upstreams("corp.com", "2.2.2.2"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
split: true,
|
||||
@@ -233,8 +233,8 @@ func TestManager(t *testing.T) {
|
||||
name: "routes-multi",
|
||||
in: Config{
|
||||
Routes: upstreams(
|
||||
"corp.com", "2.2.2.2:53",
|
||||
"bigco.net", "3.3.3.3:53"),
|
||||
"corp.com", "2.2.2.2",
|
||||
"bigco.net", "3.3.3.3"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
bs: OSConfig{
|
||||
@@ -247,17 +247,17 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(
|
||||
".", "8.8.8.8:53",
|
||||
"corp.com.", "2.2.2.2:53",
|
||||
"bigco.net.", "3.3.3.3:53"),
|
||||
".", "8.8.8.8",
|
||||
"corp.com.", "2.2.2.2",
|
||||
"bigco.net.", "3.3.3.3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "routes-multi-split",
|
||||
in: Config{
|
||||
Routes: upstreams(
|
||||
"corp.com", "2.2.2.2:53",
|
||||
"bigco.net", "3.3.3.3:53"),
|
||||
"corp.com", "2.2.2.2",
|
||||
"bigco.net", "3.3.3.3"),
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf"),
|
||||
},
|
||||
split: true,
|
||||
@@ -268,8 +268,8 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(
|
||||
"corp.com.", "2.2.2.2:53",
|
||||
"bigco.net.", "3.3.3.3:53"),
|
||||
"corp.com.", "2.2.2.2",
|
||||
"bigco.net.", "3.3.3.3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -290,7 +290,7 @@ func TestManager(t *testing.T) {
|
||||
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(".", "8.8.8.8:53"),
|
||||
Routes: upstreams(".", "8.8.8.8"),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
@@ -322,7 +322,7 @@ func TestManager(t *testing.T) {
|
||||
{
|
||||
name: "routes-magic",
|
||||
in: Config{
|
||||
Routes: upstreams("corp.com", "2.2.2.2:53", "ts.com", ""),
|
||||
Routes: upstreams("corp.com", "2.2.2.2", "ts.com", ""),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
@@ -338,8 +338,8 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams(
|
||||
"corp.com.", "2.2.2.2:53",
|
||||
".", "8.8.8.8:53"),
|
||||
"corp.com.", "2.2.2.2",
|
||||
".", "8.8.8.8"),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
@@ -350,7 +350,7 @@ func TestManager(t *testing.T) {
|
||||
name: "routes-magic-split",
|
||||
in: Config{
|
||||
Routes: upstreams(
|
||||
"corp.com", "2.2.2.2:53",
|
||||
"corp.com", "2.2.2.2",
|
||||
"ts.com", ""),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
@@ -364,7 +364,7 @@ func TestManager(t *testing.T) {
|
||||
MatchDomains: fqdns("corp.com", "ts.com"),
|
||||
},
|
||||
rs: resolver.Config{
|
||||
Routes: upstreams("corp.com.", "2.2.2.2:53"),
|
||||
Routes: upstreams("corp.com.", "2.2.2.2"),
|
||||
Hosts: hosts(
|
||||
"dave.ts.com.", "1.2.3.4",
|
||||
"bradfitz.ts.com.", "2.3.4.5"),
|
||||
@@ -393,6 +393,14 @@ func TestManager(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
|
||||
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string {
|
||||
if ipp.Port() == 53 {
|
||||
return ipp.IP().String()
|
||||
}
|
||||
return ipp.String()
|
||||
})
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := fakeOSConfigurator{
|
||||
@@ -405,8 +413,6 @@ func TestManager(t *testing.T) {
|
||||
if err := m.Set(test.in); err != nil {
|
||||
t.Fatalf("m.Set: %v", err)
|
||||
}
|
||||
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
|
||||
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string { return ipp.String() })
|
||||
if diff := cmp.Diff(f.OSConfig, test.os, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("wrong OSConfig (-got+want)\n%s", diff)
|
||||
}
|
||||
@@ -503,6 +509,11 @@ func upstreams(strs ...string) (ret map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
panic("IPPort provided before suffix")
|
||||
}
|
||||
ret[key] = append(ret[key], dnstype.Resolver{Addr: ipp.String()})
|
||||
} else if _, err := netaddr.ParseIP(s); err == nil {
|
||||
if key == "" {
|
||||
panic("IPPort provided before suffix")
|
||||
}
|
||||
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
|
||||
} else if strings.HasPrefix(s, "http") {
|
||||
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
|
||||
} else {
|
||||
|
||||
@@ -49,9 +49,9 @@ func TestDoH(t *testing.T) {
|
||||
dohSem: make(chan struct{}, 10),
|
||||
}
|
||||
|
||||
for ip := range publicdns.KnownDoH() {
|
||||
t.Run(ip.String(), func(t *testing.T) {
|
||||
urlBase, c, ok := f.getKnownDoHClient(ip)
|
||||
for urlBase := range publicdns.DoHIPsOfBase() {
|
||||
t.Run(urlBase, func(t *testing.T) {
|
||||
c, ok := f.getKnownDoHClientForProvider(urlBase)
|
||||
if !ok {
|
||||
t.Fatal("expected DoH")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -24,8 +25,10 @@ import (
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/dns/publicdns"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -47,6 +50,11 @@ const (
|
||||
// arbitrary.
|
||||
dohTransportTimeout = 30 * time.Second
|
||||
|
||||
// dohTransportTimeout is how much of a head start to give a DoH query
|
||||
// that was upgraded from a well-known public DNS provider's IP before
|
||||
// normal UDP mode is attempted as a fallback.
|
||||
dohHeadStart = 500 * time.Millisecond
|
||||
|
||||
// wellKnownHostBackupDelay is how long to artificially delay upstream
|
||||
// DNS queries to the "fallback" DNS server IP for a known provider
|
||||
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
|
||||
@@ -226,65 +234,55 @@ func (f *forwarder) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolversWithDelays maps from a set of DNS server names to a slice of
|
||||
// a type that included a startDelay. So if resolvers contains e.g. four
|
||||
// Google DNS IPs (two IPv4 + twoIPv6), this function partition adds
|
||||
// delays to some.
|
||||
// resolversWithDelays maps from a set of DNS server names to a slice of a type
|
||||
// that included a startDelay, upgrading any well-known DoH (DNS-over-HTTP)
|
||||
// servers in the process, insert a DoH lookup first before UDP fallbacks.
|
||||
func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
|
||||
rr := make([]resolverAndDelay, 0, len(resolvers)+2)
|
||||
|
||||
// Add the known DoH ones first, starting immediately.
|
||||
didDoH := map[string]bool{}
|
||||
for _, r := range resolvers {
|
||||
ipp, ok := r.IPPort()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dohBase, ok := publicdns.KnownDoH()[ipp.IP()]
|
||||
if !ok || didDoH[dohBase] {
|
||||
continue
|
||||
}
|
||||
didDoH[dohBase] = true
|
||||
rr = append(rr, resolverAndDelay{name: dnstype.Resolver{Addr: dohBase}})
|
||||
}
|
||||
|
||||
type hostAndFam struct {
|
||||
host string // some arbitrary string representing DNS host (currently the DoH base)
|
||||
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
|
||||
}
|
||||
|
||||
// Track how many of each known resolver host are in the list,
|
||||
// per address family.
|
||||
total := map[hostAndFam]int{}
|
||||
|
||||
rr := make([]resolverAndDelay, len(resolvers))
|
||||
for _, r := range resolvers {
|
||||
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
|
||||
if host, ok := publicdns.KnownDoH()[ip]; ok {
|
||||
total[hostAndFam{host, ip.BitLen()}]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done := map[hostAndFam]int{}
|
||||
for i, r := range resolvers {
|
||||
var startDelay time.Duration
|
||||
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
|
||||
if host, ok := publicdns.KnownDoH()[ip]; ok {
|
||||
key4 := hostAndFam{host, 32}
|
||||
key6 := hostAndFam{host, 128}
|
||||
switch {
|
||||
case ip.Is4():
|
||||
if done[key4] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
case ip.Is6():
|
||||
total4 := total[key4]
|
||||
if total4 >= 2 {
|
||||
// If we have two IPv4 IPs of the same provider
|
||||
// already in the set, delay the IPv6 queries
|
||||
// until halfway through the timeout (so wait
|
||||
// 2.5 seconds). Even the network is IPv6-only,
|
||||
// the DoH dialer will fallback to IPv6
|
||||
// immediately anyway.
|
||||
startDelay = responseTimeout / 2
|
||||
} else if total4 == 1 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
if done[key6] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
}
|
||||
done[hostAndFam{host, ip.BitLen()}]++
|
||||
}
|
||||
for _, r := range resolvers {
|
||||
ipp, ok := r.IPPort()
|
||||
if !ok {
|
||||
// Pass non-IP ones through unchanged, without delay.
|
||||
// (e.g. DNS-over-ExitDNS when using an exit node)
|
||||
rr = append(rr, resolverAndDelay{name: r})
|
||||
continue
|
||||
}
|
||||
rr[i] = resolverAndDelay{
|
||||
ip := ipp.IP()
|
||||
var startDelay time.Duration
|
||||
if host, ok := publicdns.KnownDoH()[ip]; ok {
|
||||
// We already did the DoH query early. These
|
||||
startDelay = dohHeadStart
|
||||
key := hostAndFam{host, ip.BitLen()}
|
||||
if done[key] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
done[key]++
|
||||
}
|
||||
rr = append(rr, resolverAndDelay{
|
||||
name: r,
|
||||
startDelay: startDelay,
|
||||
}
|
||||
})
|
||||
}
|
||||
return rr
|
||||
}
|
||||
@@ -332,21 +330,30 @@ func (f *forwarder) packetListener(ip netaddr.IP) (packetListener, error) {
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Client, ok bool) {
|
||||
urlBase, ok = publicdns.KnownDoH()[ip]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// getKnownDoHClientForProvider returns an HTTP client for a specific DoH
|
||||
// provider named by its DoH base URL (like "https://dns.google/dns-query").
|
||||
//
|
||||
// The returned client race/Happy Eyeballs dials all IPs for urlBase (usually
|
||||
// 4), as statically known by the publicdns package.
|
||||
func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client, ok bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if c, ok := f.dohClient[urlBase]; ok {
|
||||
return urlBase, c, true
|
||||
return c, true
|
||||
}
|
||||
if f.dohClient == nil {
|
||||
f.dohClient = map[string]*http.Client{}
|
||||
allIPs := publicdns.DoHIPsOfBase()[urlBase]
|
||||
if len(allIPs) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
dohURL, err := url.Parse(urlBase)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
nsDialer := netns.NewDialer(f.logf)
|
||||
dialer := dnscache.Dialer(nsDialer.DialContext, &dnscache.Resolver{
|
||||
SingleHost: dohURL.Hostname(),
|
||||
SingleHostStaticResult: allIPs,
|
||||
})
|
||||
c = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
IdleConnTimeout: dohTransportTimeout,
|
||||
@@ -354,21 +361,15 @@ func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Cl
|
||||
if !strings.HasPrefix(netw, "tcp") {
|
||||
return nil, fmt.Errorf("unexpected network %q", netw)
|
||||
}
|
||||
c, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), "443"))
|
||||
// If v4 failed, try an equivalent v6 also in the time remaining.
|
||||
if err != nil && ctx.Err() == nil {
|
||||
if ip6, ok := publicdns.DoHV6(urlBase); ok && ip.Is4() {
|
||||
if c6, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip6.String(), "443")); err == nil {
|
||||
return c6, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return c, err
|
||||
return dialer(ctx, netw, addr)
|
||||
},
|
||||
},
|
||||
}
|
||||
if f.dohClient == nil {
|
||||
f.dohClient = map[string]*http.Client{}
|
||||
}
|
||||
f.dohClient[urlBase] = c
|
||||
return urlBase, c, true
|
||||
return c, true
|
||||
}
|
||||
|
||||
const dohType = "application/dns-message"
|
||||
@@ -422,34 +423,43 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
|
||||
return res, err
|
||||
}
|
||||
|
||||
var verboseDNSForward = envknob.Bool("TS_DEBUG_DNS_FORWARD_SEND")
|
||||
|
||||
// send sends packet to dst. It is best effort.
|
||||
//
|
||||
// send expects the reply to have the same txid as txidOut.
|
||||
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
|
||||
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
|
||||
if verboseDNSForward {
|
||||
f.logf("forwarder.send(%q) ...", rr.name.Addr)
|
||||
defer func() {
|
||||
f.logf("forwarder.send(%q) = %v, %v", rr.name.Addr, len(ret), err)
|
||||
}()
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "http://") {
|
||||
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "https://") {
|
||||
// Only known DoH providers are supported currently. Specifically, we
|
||||
// only support DoH providers where we can TCP connect to them on port
|
||||
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
|
||||
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custon DoH providers
|
||||
// aren't currently supported. There's no backup DNS resolution path for
|
||||
// them.
|
||||
urlBase := rr.name.Addr
|
||||
if hc, ok := f.getKnownDoHClientForProvider(urlBase); ok {
|
||||
return f.sendDoH(ctx, urlBase, hc, fq.packet)
|
||||
}
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
return nil, fmt.Errorf("https:// resolvers not supported yet")
|
||||
return nil, fmt.Errorf("arbitrary https:// resolvers not supported yet")
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "tls://") {
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
return nil, fmt.Errorf("tls:// resolvers not supported yet")
|
||||
}
|
||||
ipp, err := netaddr.ParseIPPort(rr.name.Addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Upgrade known DNS IPs to DoH (DNS-over-HTTPs).
|
||||
// All known DoH is over port 53.
|
||||
if urlBase, dc, ok := f.getKnownDoHClient(ipp.IP()); ok {
|
||||
res, err := f.sendDoH(ctx, urlBase, dc, fq.packet)
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return res, err
|
||||
}
|
||||
f.logf("DoH error from %v: %v", ipp.IP(), err)
|
||||
ipp, ok := rr.name.IPPort()
|
||||
if !ok {
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
|
||||
}
|
||||
|
||||
metricDNSFwdUDP.Add(1)
|
||||
@@ -593,6 +603,10 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
return err
|
||||
}
|
||||
|
||||
// Guarantee that the ctx we use below is done when this function returns.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Drop DNS service discovery spam, primarily for battery life
|
||||
// on mobile. Things like Spotify on iOS generate this traffic,
|
||||
// when browsing for LAN devices. But even when filtering this
|
||||
@@ -633,12 +647,8 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
}
|
||||
defer fq.closeOnCtxDone.Close()
|
||||
|
||||
resc := make(chan []byte, 1)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
firstErr error
|
||||
)
|
||||
|
||||
resc := make(chan []byte, 1) // it's fine buffered or not
|
||||
errc := make(chan error, 1) // it's fine buffered or not too
|
||||
for i := range resolvers {
|
||||
go func(rr *resolverAndDelay) {
|
||||
if rr.startDelay > 0 {
|
||||
@@ -652,39 +662,48 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
}
|
||||
resb, err := f.send(ctx, fq, *rr)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
select {
|
||||
case errc <- err:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case resc <- resb:
|
||||
default:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}(&resolvers[i])
|
||||
}
|
||||
|
||||
select {
|
||||
case v := <-resc:
|
||||
var firstErr error
|
||||
var numErr int
|
||||
for {
|
||||
select {
|
||||
case v := <-resc:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
metricDNSFwdErrorContext.Add(1)
|
||||
return ctx.Err()
|
||||
case responseChan <- packet{v, query.addr}:
|
||||
metricDNSFwdSuccess.Add(1)
|
||||
return nil
|
||||
}
|
||||
case err := <-errc:
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
numErr++
|
||||
if numErr == len(resolvers) {
|
||||
return firstErr
|
||||
}
|
||||
case <-ctx.Done():
|
||||
metricDNSFwdErrorContext.Add(1)
|
||||
if firstErr != nil {
|
||||
metricDNSFwdErrorContextGotError.Add(1)
|
||||
return firstErr
|
||||
}
|
||||
return ctx.Err()
|
||||
case responseChan <- packet{v, query.addr}:
|
||||
metricDNSFwdSuccess.Add(1)
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
metricDNSFwdErrorContext.Add(1)
|
||||
if firstErr != nil {
|
||||
metricDNSFwdErrorContextGotError.Add(1)
|
||||
return firstErr
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -24,11 +24,7 @@ func (rr resolverAndDelay) String() string {
|
||||
func TestResolversWithDelays(t *testing.T) {
|
||||
// query
|
||||
q := func(ss ...string) (ipps []dnstype.Resolver) {
|
||||
for _, s := range ss {
|
||||
host, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, host := range ss {
|
||||
ipps = append(ipps, dnstype.Resolver{Addr: host})
|
||||
}
|
||||
return
|
||||
@@ -45,12 +41,8 @@ func TestResolversWithDelays(t *testing.T) {
|
||||
panic(fmt.Sprintf("parsing duration in %q: %v", s, err))
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr = append(rr, resolverAndDelay{
|
||||
name: dnstype.Resolver{Addr: host},
|
||||
name: dnstype.Resolver{Addr: s},
|
||||
startDelay: d,
|
||||
})
|
||||
}
|
||||
@@ -64,28 +56,28 @@ func TestResolversWithDelays(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "unknown-no-delays",
|
||||
in: q("1.2.3.4:53", "2.3.4.5:53"),
|
||||
want: o("1.2.3.4:53", "2.3.4.5:53"),
|
||||
in: q("1.2.3.4", "2.3.4.5"),
|
||||
want: o("1.2.3.4", "2.3.4.5"),
|
||||
},
|
||||
{
|
||||
name: "google-all-ipv4",
|
||||
in: q("8.8.8.8:53", "8.8.4.4:53"),
|
||||
want: o("8.8.8.8:53", "8.8.4.4:53+200ms"),
|
||||
in: q("8.8.8.8", "8.8.4.4"),
|
||||
want: o("https://dns.google/dns-query", "8.8.8.8+0.5s", "8.8.4.4+0.7s"),
|
||||
},
|
||||
{
|
||||
name: "google-only-ipv6",
|
||||
in: q("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
|
||||
want: o("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53+200ms"),
|
||||
in: q("2001:4860:4860::8888", "2001:4860:4860::8844"),
|
||||
want: o("https://dns.google/dns-query", "2001:4860:4860::8888+0.5s", "2001:4860:4860::8844+0.7s"),
|
||||
},
|
||||
{
|
||||
name: "google-all-four",
|
||||
in: q("8.8.8.8:53", "8.8.4.4:53", "[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
|
||||
want: o("8.8.8.8:53", "8.8.4.4:53+200ms", "[2001:4860:4860::8888]:53+2.5s", "[2001:4860:4860::8844]:53+2.7s"),
|
||||
in: q("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844"),
|
||||
want: o("https://dns.google/dns-query", "8.8.8.8+0.5s", "8.8.4.4+0.7s", "2001:4860:4860::8888+0.5s", "2001:4860:4860::8844+0.7s"),
|
||||
},
|
||||
{
|
||||
name: "quad9-one-v4-one-v6",
|
||||
in: q("9.9.9.9:53", "[2620:fe::fe]:53"),
|
||||
want: o("9.9.9.9:53", "[2620:fe::fe]:53+200ms"),
|
||||
in: q("9.9.9.9", "2620:fe::fe"),
|
||||
want: o("https://dns.quad9.net/dns-query", "9.9.9.9+0.5s", "2620:fe::fe+0.5s"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,6 +161,25 @@ func TestMaxDoHInFlight(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var testDNS = flag.Bool("test-dns", false, "run tests that require a working DNS server")
|
||||
|
||||
func TestGetKnownDoHClientForProvider(t *testing.T) {
|
||||
var fwd forwarder
|
||||
c, ok := fwd.getKnownDoHClientForProvider("https://dns.google/dns-query")
|
||||
if !ok {
|
||||
t.Fatal("not found")
|
||||
}
|
||||
if !*testDNS {
|
||||
t.Skip("skipping without --test-dns")
|
||||
}
|
||||
res, err := c.Head("https://dns.google/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
t.Logf("Got: %+v", res)
|
||||
}
|
||||
|
||||
func BenchmarkNameFromQuery(b *testing.B) {
|
||||
builder := dns.NewBuilder(nil, dns.Header{})
|
||||
builder.StartQuestions()
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -633,6 +634,10 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
|
||||
return tsaddr.TailscaleServiceIPv6(), dns.RCodeSuccess
|
||||
}
|
||||
}
|
||||
// Special-case: 'via-<siteid>.<ipv4>' queries.
|
||||
if ip, ok := r.parseViaDomain(domain, typ); ok {
|
||||
return ip, dns.RCodeSuccess
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
hosts := r.hostToIP
|
||||
@@ -708,6 +713,46 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
|
||||
}
|
||||
}
|
||||
|
||||
// parseViaDomain synthesizes an IP address for quad-A DNS requests of
|
||||
// the form 'via-<X>.<IPv4-address>', where X is a decimal, or hex-encoded
|
||||
// number with a '0x' prefix.
|
||||
//
|
||||
// This exists as a convenient mapping into Tailscales 'Via Range'.
|
||||
func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netaddr.IP, bool) {
|
||||
fqdn := string(domain.WithoutTrailingDot())
|
||||
if typ != dns.TypeAAAA {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
if len(fqdn) < len("via-X.0.0.0.0") {
|
||||
return netaddr.IP{}, false // too short to be valid
|
||||
}
|
||||
if !strings.HasPrefix(fqdn, "via-") {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
|
||||
firstDot := strings.Index(fqdn, ".")
|
||||
if firstDot < 0 {
|
||||
return netaddr.IP{}, false // missing dot delimiters
|
||||
}
|
||||
|
||||
siteID := fqdn[len("via-"):firstDot]
|
||||
ip4Str := fqdn[firstDot+1:]
|
||||
|
||||
ip4, err := netaddr.ParseIP(ip4Str)
|
||||
if err != nil {
|
||||
return netaddr.IP{}, false // badly formed, dont respond
|
||||
}
|
||||
|
||||
prefix, err := strconv.ParseUint(siteID, 0, 32)
|
||||
if err != nil {
|
||||
return netaddr.IP{}, false // badly formed, dont respond
|
||||
}
|
||||
|
||||
// MapVia will never error when given an ipv4 netaddr.IPPrefix.
|
||||
out, _ := tsaddr.MapVia(uint32(prefix), netaddr.IPPrefixFrom(ip4, ip4.BitLen()))
|
||||
return out.IP(), true
|
||||
}
|
||||
|
||||
// resolveReverse returns the unique domain name that maps to the given address.
|
||||
func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
|
||||
var ip netaddr.IP
|
||||
@@ -725,6 +770,22 @@ func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCo
|
||||
return "", dns.RCodeRefused
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// If the requested IP is part of the IPv6 4-to-6 range, it might
|
||||
// correspond to an IPv4 address (assuming IPv4 is enabled).
|
||||
if ip4, ok := tsaddr.Tailscale6to4(ip); ok {
|
||||
fqdn, code := r.fqdnForIPLocked(ip4, name)
|
||||
if code == dns.RCodeSuccess {
|
||||
return fqdn, code
|
||||
}
|
||||
}
|
||||
return r.fqdnForIPLocked(ip, name)
|
||||
}
|
||||
|
||||
// r.mu must be held.
|
||||
func (r *Resolver) fqdnForIPLocked(ip netaddr.IP, name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
|
||||
// If someone curiously does a reverse lookup on the DNS IP, we
|
||||
// return a domain that helps indicate that Tailscale is using
|
||||
// this IP for a special purpose and it is not a node on their
|
||||
@@ -733,8 +794,6 @@ func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCo
|
||||
return dnsSymbolicFQDN, dns.RCodeSuccess
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
ret, ok := r.ipToHost[ip]
|
||||
if !ok {
|
||||
for _, suffix := range r.localDomains {
|
||||
|
||||
@@ -348,6 +348,9 @@ func TestResolveLocal(t *testing.T) {
|
||||
{"ns-nxdomain", "test3.ipn.dev.", dns.TypeNS, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"onion-domain", "footest.onion.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"magicdns", dnsSymbolicFQDN, dns.TypeA, netaddr.MustParseIP("100.100.100.100"), dns.RCodeSuccess},
|
||||
{"via_hex", dnsname.FQDN("via-0xff.1.2.3.4."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:ff:102:304"), dns.RCodeSuccess},
|
||||
{"via_dec", dnsname.FQDN("via-1.10.0.0.1."), dns.TypeAAAA, netaddr.MustParseIP("fd7a:115c:a1e0:b1a:0:1:a00:1"), dns.RCodeSuccess},
|
||||
{"via_invalid", dnsname.FQDN("via-."), dns.TypeA, netaddr.IP{}, dns.RCodeRefused},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -382,6 +385,7 @@ func TestResolveLocalReverse(t *testing.T) {
|
||||
{"ipv6_nxdomain", dnsname.FQDN("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.ip6.arpa."), "", dns.RCodeNameError},
|
||||
{"nxdomain", dnsname.FQDN("2.3.4.5.in-addr.arpa."), "", dns.RCodeRefused},
|
||||
{"magicdns", dnsname.FQDN("100.100.100.100.in-addr.arpa."), dnsSymbolicFQDN, dns.RCodeSuccess},
|
||||
{"ipv6_4to6", dnsname.FQDN("4.6.4.6.4.6.2.6.6.9.d.c.3.4.8.4.2.1.b.a.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), dnsSymbolicFQDN, dns.RCodeSuccess},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -755,6 +759,9 @@ func TestDelegateCollision(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(ans) == 0 {
|
||||
t.Fatal("no answers")
|
||||
}
|
||||
|
||||
var wantType dns.Type
|
||||
switch ans[0].Body.(type) {
|
||||
|
||||
@@ -72,6 +72,15 @@ type Resolver struct {
|
||||
// if a refresh fails.
|
||||
UseLastGood bool
|
||||
|
||||
// SingleHostStaticResult, if non-nil, is the static result of IPs that is returned
|
||||
// by Resolver.LookupIP for any hostname. When non-nil, SingleHost must also be
|
||||
// set with the expected name.
|
||||
SingleHostStaticResult []netaddr.IP
|
||||
|
||||
// SingleHost is the hostname that SingleHostStaticResult is for.
|
||||
// It is required when SingleHostStaticResult is present.
|
||||
SingleHost string
|
||||
|
||||
sf singleflight.Group
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -108,6 +117,22 @@ var debug = envknob.Bool("TS_DEBUG_DNS_CACHE")
|
||||
// If err is nil, ip will be non-nil. The v6 address may be nil even
|
||||
// with a nil error.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, allIPs []net.IPAddr, err error) {
|
||||
if r.SingleHostStaticResult != nil {
|
||||
if r.SingleHost != host {
|
||||
return nil, nil, nil, fmt.Errorf("dnscache: unexpected hostname %q doesn't match expected %q", host, r.SingleHost)
|
||||
}
|
||||
for _, naIP := range r.SingleHostStaticResult {
|
||||
ipa := naIP.IPAddr()
|
||||
if ip == nil && naIP.Is4() {
|
||||
ip = ipa.IP
|
||||
}
|
||||
if v6 == nil && naIP.Is6() {
|
||||
v6 = ipa.IP
|
||||
}
|
||||
allIPs = append(allIPs, *ipa)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil, []net.IPAddr{{IP: ip4}}, nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -110,3 +111,33 @@ func TestDialCall_uniqueIPs(t *testing.T) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverAllHostStaticResult(t *testing.T) {
|
||||
r := &Resolver{
|
||||
SingleHost: "foo.bar",
|
||||
SingleHostStaticResult: []netaddr.IP{
|
||||
netaddr.MustParseIP("2001:4860:4860::8888"),
|
||||
netaddr.MustParseIP("2001:4860:4860::8844"),
|
||||
netaddr.MustParseIP("8.8.8.8"),
|
||||
netaddr.MustParseIP("8.8.4.4"),
|
||||
},
|
||||
}
|
||||
ip4, ip6, allIPs, err := r.LookupIP(context.Background(), "foo.bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := ip4.String(), "8.8.8.8"; got != want {
|
||||
t.Errorf("ip4 got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := ip6.String(), "2001:4860:4860::8888"; got != want {
|
||||
t.Errorf("ip4 got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := fmt.Sprintf("%q", allIPs), `[{"2001:4860:4860::8888" ""} {"2001:4860:4860::8844" ""} {"8.8.8.8" ""} {"8.8.4.4" ""}]`; got != want {
|
||||
t.Errorf("allIPs got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
_, _, _, err = r.LookupIP(context.Background(), "bad")
|
||||
if got, want := fmt.Sprint(err), `dnscache: unexpected hostname "bad" doesn't match expected "foo.bar"`; got != want {
|
||||
t.Errorf("bad dial error got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,25 @@
|
||||
"RegionName": "r1",
|
||||
"Nodes": [
|
||||
{
|
||||
"Name": "1a",
|
||||
"Name": "1c",
|
||||
"RegionID": 1,
|
||||
"HostName": "derp1.tailscale.com",
|
||||
"IPv4": "159.89.225.99",
|
||||
"IPv6": "2604:a880:400:d1::828:b001"
|
||||
"HostName": "derp1c.tailscale.com",
|
||||
"IPv4": "104.248.8.210",
|
||||
"IPv6": "2604:a880:800:10::7a0:e001"
|
||||
},
|
||||
{
|
||||
"Name": "1b",
|
||||
"Name": "1d",
|
||||
"RegionID": 1,
|
||||
"HostName": "derp1b.tailscale.com",
|
||||
"IPv4": "45.55.35.93",
|
||||
"IPv6": "2604:a880:800:a1::f:2001"
|
||||
"HostName": "derp1d.tailscale.com",
|
||||
"IPv4": "165.22.33.71",
|
||||
"IPv6": "2604:a880:800:10::7fe:f001"
|
||||
},
|
||||
{
|
||||
"Name": "1e",
|
||||
"RegionID": 1,
|
||||
"HostName": "derp1e.tailscale.com",
|
||||
"IPv4": "64.225.56.166",
|
||||
"IPv6": "2604:a880:800:10::873:4001"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -979,13 +979,21 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
|
||||
// One warm-up one to get HTTP connection set
|
||||
// up and get a connection from the browser's
|
||||
// pool.
|
||||
if _, err := http.DefaultClient.Do(req); err != nil {
|
||||
c.logf("probing %s: %v", node.HostName, err)
|
||||
if r, err := http.DefaultClient.Do(req); err != nil || r.StatusCode > 299 {
|
||||
if err != nil {
|
||||
c.logf("probing %s: %v", node.HostName, err)
|
||||
} else {
|
||||
c.logf("probing %s: unexpected status %s", node.HostName, r.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
t0 := c.timeNow()
|
||||
if _, err := http.DefaultClient.Do(req); err != nil {
|
||||
c.logf("probing %s: %v", node.HostName, err)
|
||||
if r, err := http.DefaultClient.Do(req); err != nil || r.StatusCode > 299 {
|
||||
if err != nil {
|
||||
c.logf("probing %s: %v", node.HostName, err)
|
||||
} else {
|
||||
c.logf("probing %s: unexpected status %s", node.HostName, r.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
d := c.timeNow().Sub(t0)
|
||||
@@ -1005,7 +1013,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
var ip netaddr.IP
|
||||
|
||||
dc := derphttp.NewNetcheckClient(c.logf)
|
||||
tlsConn, tcpConn, err := dc.DialRegionTLS(ctx, reg)
|
||||
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
}
|
||||
@@ -1036,7 +1044,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
}
|
||||
hc := &http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://derp-unused-hostname.tld/derp/latency-check", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://" + node.HostName + "/derp/latency-check", nil)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
}
|
||||
@@ -1047,6 +1055,13 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// DERPs should give us a nominal status code, so anything else is probably
|
||||
// an access denied by a MITM proxy (or at the very least a signal not to
|
||||
// trust this latency check).
|
||||
if resp.StatusCode > 299 {
|
||||
return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
_, err = io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8<<10))
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
package tsaddr
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -126,6 +128,18 @@ func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
|
||||
return netaddr.IPFrom16(ret)
|
||||
}
|
||||
|
||||
// Tailscale6to4 returns the IPv4 address corresponding to the given
|
||||
// tailscale IPv6 address within the 4To6 range. The IPv4 address
|
||||
// and true are returned if the given address was in the correct range,
|
||||
// false if not.
|
||||
func Tailscale6to4(ipv6 netaddr.IP) (netaddr.IP, bool) {
|
||||
if !ipv6.Is6() || !Tailscale4To6Range().Contains(ipv6) {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
v6 := ipv6.As16()
|
||||
return netaddr.IPv4(100, v6[13], v6[14], v6[15]), true
|
||||
}
|
||||
|
||||
func mustPrefix(v *netaddr.IPPrefix, prefix string) {
|
||||
var err error
|
||||
*v, err = netaddr.ParseIPPrefix(prefix)
|
||||
@@ -280,3 +294,17 @@ func UnmapVia(ip netaddr.IP) netaddr.IP {
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
// MapVia returns an IPv6 "via" route for an IPv4 CIDR in a given siteID.
|
||||
func MapVia(siteID uint32, v4 netaddr.IPPrefix) (via netaddr.IPPrefix, err error) {
|
||||
if !v4.IP().Is4() {
|
||||
return via, errors.New("want IPv4 CIDR with a site ID")
|
||||
}
|
||||
viaRange16 := TailscaleViaRange().IP().As16()
|
||||
var a [16]byte
|
||||
copy(a[:], viaRange16[:8])
|
||||
binary.BigEndian.PutUint32(a[8:], siteID)
|
||||
ip4a := v4.IP().As4()
|
||||
copy(a[12:], ip4a[:])
|
||||
return netaddr.IPPrefixFrom(netaddr.IPFrom16(a), v4.Bits()+64+32), nil
|
||||
}
|
||||
|
||||
@@ -20,8 +20,12 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -30,6 +34,7 @@ import (
|
||||
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
|
||||
// Extension, none), user-selected route acceptance prefs, etc.
|
||||
type Dialer struct {
|
||||
Logf logger.Logf
|
||||
// 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
|
||||
@@ -46,12 +51,33 @@ type Dialer struct {
|
||||
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
|
||||
netnsDialerOnce sync.Once
|
||||
netnsDialer netns.Dialer
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
linkMon *monitor.Mon
|
||||
linkMonUnregister func()
|
||||
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
|
||||
nextSysConnID int
|
||||
activeSysConns map[int]net.Conn // active connections not yet closed
|
||||
}
|
||||
|
||||
// sysConn wraps a net.Conn that was created using d.SystemDial.
|
||||
// It exists to track which connections are still open, and should be
|
||||
// closed on major link changes.
|
||||
type sysConn struct {
|
||||
net.Conn
|
||||
id int
|
||||
d *Dialer
|
||||
}
|
||||
|
||||
func (c sysConn) Close() error {
|
||||
c.d.closeSysConn(c.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
|
||||
@@ -91,10 +117,53 @@ func (d *Dialer) SetExitDNSDoH(doh string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) Close() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.closed = true
|
||||
if d.linkMonUnregister != nil {
|
||||
d.linkMonUnregister()
|
||||
d.linkMonUnregister = nil
|
||||
}
|
||||
for _, c := range d.activeSysConns {
|
||||
c.Close()
|
||||
}
|
||||
d.activeSysConns = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.linkMonUnregister != nil {
|
||||
go d.linkMonUnregister()
|
||||
d.linkMonUnregister = nil
|
||||
}
|
||||
d.linkMon = mon
|
||||
d.linkMonUnregister = d.linkMon.RegisterChangeCallback(d.linkChanged)
|
||||
}
|
||||
|
||||
func (d *Dialer) linkChanged(major bool, state *interfaces.State) {
|
||||
if !major {
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for id, c := range d.activeSysConns {
|
||||
go c.Close()
|
||||
delete(d.activeSysConns, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) closeSysConn(id int) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
c, ok := d.activeSysConns[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(d.activeSysConns, id)
|
||||
go c.Close() // ignore the error
|
||||
}
|
||||
|
||||
func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
|
||||
@@ -197,6 +266,42 @@ func ipNetOfNetwork(n string) string {
|
||||
return "ip"
|
||||
}
|
||||
|
||||
// SystemDial connects to the provided network address without going over
|
||||
// Tailscale. It prefers going over the default interface and closes existing
|
||||
// connections if the default interface changes. It is used to connect to
|
||||
// Control and (in the future, as of 2022-04-27) DERPs..
|
||||
func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
d.mu.Lock()
|
||||
closed := d.closed
|
||||
d.mu.Unlock()
|
||||
if closed {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
d.netnsDialerOnce.Do(func() {
|
||||
logf := d.Logf
|
||||
if logf == nil {
|
||||
logf = logger.Discard
|
||||
}
|
||||
d.netnsDialer = netns.NewDialer(logf)
|
||||
})
|
||||
c, err := d.netnsDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
id := d.nextSysConnID
|
||||
d.nextSysConnID++
|
||||
mak.Set(&d.activeSysConns, id, c)
|
||||
|
||||
return sysConn{
|
||||
id: id,
|
||||
d: d,
|
||||
Conn: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
26
net/tshttpproxy/tshttpproxy_linux.go
Normal file
26
net/tshttpproxy/tshttpproxy_linux.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sysProxyFromEnv = linuxSysProxyFromEnv
|
||||
}
|
||||
|
||||
func linuxSysProxyFromEnv(req *http.Request) (*url.URL, error) {
|
||||
if distro.Get() == distro.Synology {
|
||||
return synologyProxyFromConfigCached(req)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
142
net/tshttpproxy/tshttpproxy_synology.go
Normal file
142
net/tshttpproxy/tshttpproxy_synology.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
// These vars are overridden for tests.
|
||||
var (
|
||||
synologyProxyConfigPath = "/etc/proxy.conf"
|
||||
|
||||
openSynologyProxyConf = func() (io.ReadCloser, error) {
|
||||
return os.Open(synologyProxyConfigPath)
|
||||
}
|
||||
)
|
||||
|
||||
var cache struct {
|
||||
sync.Mutex
|
||||
httpProxy *url.URL
|
||||
httpsProxy *url.URL
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
|
||||
if req.URL == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
var err error
|
||||
modtime := mtime(synologyProxyConfigPath)
|
||||
|
||||
if modtime != cache.updated {
|
||||
cache.httpProxy, cache.httpsProxy, err = synologyProxiesFromConfig()
|
||||
cache.updated = modtime
|
||||
}
|
||||
|
||||
if req.URL.Scheme == "https" {
|
||||
return cache.httpsProxy, err
|
||||
}
|
||||
return cache.httpProxy, err
|
||||
}
|
||||
|
||||
func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
|
||||
r, err := openSynologyProxyConf()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return parseSynologyConfig(r)
|
||||
}
|
||||
|
||||
// parseSynologyConfig parses the Synology proxy configuration, and returns any
|
||||
// http proxy, and any https proxy respectively, or an error if parsing fails.
|
||||
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
|
||||
cfg := map[string]string{}
|
||||
|
||||
if err := lineread.Reader(r, func(line []byte) error {
|
||||
// accept and skip over empty lines
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, value, ok := strings.Cut(string(line), "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
|
||||
}
|
||||
cfg[string(key)] = string(value)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if cfg["proxy_enabled"] != "yes" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
httpProxyURL := new(url.URL)
|
||||
httpsProxyURL := new(url.URL)
|
||||
if cfg["auth_enabled"] == "yes" {
|
||||
httpProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
|
||||
httpsProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
|
||||
}
|
||||
|
||||
// As far as we are aware, synology does not support tls proxies.
|
||||
httpProxyURL.Scheme = "http"
|
||||
httpsProxyURL.Scheme = "http"
|
||||
|
||||
httpsProxyURL = addHostPort(httpsProxyURL, cfg["https_host"], cfg["https_port"])
|
||||
httpProxyURL = addHostPort(httpProxyURL, cfg["http_host"], cfg["http_port"])
|
||||
|
||||
return httpProxyURL, httpsProxyURL, nil
|
||||
}
|
||||
|
||||
// addHostPort adds to u the given host and port and returns the updated url, or
|
||||
// if host is empty, it returns nil.
|
||||
func addHostPort(u *url.URL, host, port string) *url.URL {
|
||||
if host == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
u.Host = host
|
||||
} else {
|
||||
u.Host = net.JoinHostPort(host, port)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// mtime stat's path and returns its modification time. If path does not exist,
|
||||
// it returns the unix epoch.
|
||||
func mtime(path string) time.Time {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return time.Unix(0, 0)
|
||||
}
|
||||
return fi.ModTime()
|
||||
}
|
||||
381
net/tshttpproxy/tshttpproxy_synology_test.go
Normal file
381
net/tshttpproxy/tshttpproxy_synology_test.go
Normal file
@@ -0,0 +1,381 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSynologyProxyFromConfigCached(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://example.org/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var orig string
|
||||
orig, synologyProxyConfigPath = synologyProxyConfigPath, filepath.Join(t.TempDir(), "proxy.conf")
|
||||
defer func() { synologyProxyConfigPath = orig }()
|
||||
|
||||
t.Run("no config file", func(t *testing.T) {
|
||||
if _, err := os.Stat(synologyProxyConfigPath); err == nil {
|
||||
t.Fatalf("%s must not exist for this test", synologyProxyConfigPath)
|
||||
}
|
||||
|
||||
cache.updated = time.Time{}
|
||||
cache.httpProxy = nil
|
||||
cache.httpsProxy = nil
|
||||
|
||||
if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil {
|
||||
t.Fatalf("got %s, %v; want nil, nil", val, err)
|
||||
}
|
||||
|
||||
if got, want := cache.updated, time.Unix(0, 0); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
if cache.httpProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpProxy)
|
||||
}
|
||||
if cache.httpsProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("config file updated", func(t *testing.T) {
|
||||
cache.updated = time.Now()
|
||||
cache.httpProxy = nil
|
||||
cache.httpsProxy = nil
|
||||
|
||||
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
|
||||
proxy_enabled=yes
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
https_host=10.0.0.66
|
||||
https_port=443
|
||||
`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
val, err := synologyProxyFromConfigCached(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cache.httpProxy == nil {
|
||||
t.Fatal("http proxy was not cached")
|
||||
}
|
||||
if cache.httpsProxy == nil {
|
||||
t.Fatal("https proxy was not cached")
|
||||
}
|
||||
|
||||
if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() {
|
||||
t.Fatalf("got %s; want %s", val, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("config file removed", func(t *testing.T) {
|
||||
cache.updated = time.Now()
|
||||
cache.httpProxy = urlMustParse("http://127.0.0.1/")
|
||||
cache.httpsProxy = urlMustParse("http://127.0.0.1/")
|
||||
|
||||
if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
val, err := synologyProxyFromConfigCached(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if val != nil {
|
||||
t.Fatalf("got %s; want nil", val)
|
||||
}
|
||||
if cache.httpProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpProxy)
|
||||
}
|
||||
if cache.httpsProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("picks proxy from request scheme", func(t *testing.T) {
|
||||
cache.updated = time.Now()
|
||||
cache.httpProxy = nil
|
||||
cache.httpsProxy = nil
|
||||
|
||||
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
|
||||
proxy_enabled=yes
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
https_host=10.0.0.66
|
||||
https_port=443
|
||||
`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
val, err := synologyProxyFromConfigCached(httpReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if val == nil {
|
||||
t.Fatalf("got nil, want an http URL")
|
||||
}
|
||||
if got, want := val.String(), "http://10.0.0.55:80"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
httpsReq, err := http.NewRequest("GET", "https://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
val, err = synologyProxyFromConfigCached(httpsReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if val == nil {
|
||||
t.Fatalf("got nil, want an http URL")
|
||||
}
|
||||
if got, want := val.String(), "http://10.0.0.66:443"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSynologyProxiesFromConfig(t *testing.T) {
|
||||
var (
|
||||
openReader io.ReadCloser
|
||||
openErr error
|
||||
)
|
||||
var origOpen func() (io.ReadCloser, error)
|
||||
origOpen, openSynologyProxyConf = openSynologyProxyConf, func() (io.ReadCloser, error) {
|
||||
return openReader, openErr
|
||||
}
|
||||
defer func() { openSynologyProxyConf = origOpen }()
|
||||
|
||||
t.Run("with config", func(t *testing.T) {
|
||||
mc := &mustCloser{Reader: strings.NewReader(`
|
||||
proxy_user=foo
|
||||
proxy_pwd=bar
|
||||
proxy_enabled=yes
|
||||
adv_enabled=yes
|
||||
bypass_enabled=yes
|
||||
auth_enabled=yes
|
||||
https_host=10.0.0.66
|
||||
https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`)}
|
||||
defer mc.check(t)
|
||||
openReader = mc
|
||||
|
||||
httpProxy, httpsProxy, err := synologyProxiesFromConfig()
|
||||
|
||||
if got, want := err, openErr; got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := httpsProxy, urlMustParse("http://foo:bar@10.0.0.66:8443"); got.String() != want.String() {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := err, openErr; got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := httpProxy, urlMustParse("http://foo:bar@10.0.0.55:80"); got.String() != want.String() {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("non-existent config", func(t *testing.T) {
|
||||
openReader = nil
|
||||
openErr = os.ErrNotExist
|
||||
|
||||
httpProxy, httpsProxy, err := synologyProxiesFromConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
if httpProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpProxy)
|
||||
}
|
||||
if httpsProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error opening config", func(t *testing.T) {
|
||||
openReader = nil
|
||||
openErr = errors.New("example error")
|
||||
|
||||
httpProxy, httpsProxy, err := synologyProxiesFromConfig()
|
||||
if err != openErr {
|
||||
t.Fatalf("expected %s, got %s", openErr, err)
|
||||
}
|
||||
if httpProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpProxy)
|
||||
}
|
||||
if httpsProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestParseSynologyConfig(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
httpProxy *url.URL
|
||||
httpsProxy *url.URL
|
||||
err error
|
||||
}{
|
||||
"populated": {
|
||||
input: `
|
||||
proxy_user=foo
|
||||
proxy_pwd=bar
|
||||
proxy_enabled=yes
|
||||
adv_enabled=yes
|
||||
bypass_enabled=yes
|
||||
auth_enabled=yes
|
||||
https_host=10.0.0.66
|
||||
https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`,
|
||||
httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
|
||||
httpsProxy: urlMustParse("http://foo:bar@10.0.0.66:8443"),
|
||||
err: nil,
|
||||
},
|
||||
"no-auth": {
|
||||
input: `
|
||||
proxy_user=foo
|
||||
proxy_pwd=bar
|
||||
proxy_enabled=yes
|
||||
adv_enabled=yes
|
||||
bypass_enabled=yes
|
||||
auth_enabled=no
|
||||
https_host=10.0.0.66
|
||||
https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`,
|
||||
httpProxy: urlMustParse("http://10.0.0.55:80"),
|
||||
httpsProxy: urlMustParse("http://10.0.0.66:8443"),
|
||||
err: nil,
|
||||
},
|
||||
"http-only": {
|
||||
input: `
|
||||
proxy_user=foo
|
||||
proxy_pwd=bar
|
||||
proxy_enabled=yes
|
||||
adv_enabled=yes
|
||||
bypass_enabled=yes
|
||||
auth_enabled=yes
|
||||
https_host=
|
||||
https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`,
|
||||
httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
|
||||
httpsProxy: nil,
|
||||
err: nil,
|
||||
},
|
||||
"empty": {
|
||||
input: `
|
||||
proxy_user=
|
||||
proxy_pwd=
|
||||
proxy_enabled=
|
||||
adv_enabled=
|
||||
bypass_enabled=
|
||||
auth_enabled=
|
||||
https_host=
|
||||
https_port=
|
||||
http_host=
|
||||
http_port=
|
||||
`,
|
||||
httpProxy: nil,
|
||||
httpsProxy: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, example := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
httpProxy, httpsProxy, err := parseSynologyConfig(strings.NewReader(example.input))
|
||||
if err != example.err {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if example.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if example.httpProxy == nil && httpProxy != nil {
|
||||
t.Fatalf("got %s, want nil", httpProxy)
|
||||
}
|
||||
|
||||
if example.httpProxy != nil {
|
||||
if httpProxy == nil {
|
||||
t.Fatalf("got nil, want %s", example.httpProxy)
|
||||
}
|
||||
|
||||
if got, want := example.httpProxy.String(), httpProxy.String(); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if example.httpsProxy == nil && httpsProxy != nil {
|
||||
t.Fatalf("got %s, want nil", httpProxy)
|
||||
}
|
||||
|
||||
if example.httpsProxy != nil {
|
||||
if httpsProxy == nil {
|
||||
t.Fatalf("got nil, want %s", example.httpsProxy)
|
||||
}
|
||||
|
||||
if got, want := example.httpsProxy.String(), httpsProxy.String(); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func urlMustParse(u string) *url.URL {
|
||||
r, err := url.Parse(u)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("urlMustParse: %s", err))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type mustCloser struct {
|
||||
io.Reader
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (m *mustCloser) Close() error {
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mustCloser) check(t *testing.T) {
|
||||
if !m.closed {
|
||||
t.Errorf("mustCloser wrapping %#v was not closed at time of check", m.Reader)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
@@ -114,77 +112,30 @@ func socketPermissionsForOS() os.FileMode {
|
||||
return 0600
|
||||
}
|
||||
|
||||
// connectMacOSAppSandbox connects to the Tailscale Network Extension,
|
||||
// which is necessarily running within the macOS App Sandbox. Our
|
||||
// little dance to connect a regular user binary to the sandboxed
|
||||
// network extension is:
|
||||
// connectMacOSAppSandbox connects to the Tailscale Network Extension (macOS App
|
||||
// Store build) or App Extension (macsys standalone build), where the CLI itself
|
||||
// is either running within the macOS App Sandbox or built separately (e.g.
|
||||
// homebrew or go install). This little dance to connect a regular user binary
|
||||
// to the sandboxed network extension is:
|
||||
//
|
||||
// * the sandboxed IPNExtension picks a random localhost:0 TCP port
|
||||
// to listen on
|
||||
// * it also picks a random hex string that acts as an auth token
|
||||
// * it then creates a file named "sameuserproof-$PORT-$TOKEN" and leaves
|
||||
// that file descriptor open forever.
|
||||
//
|
||||
// Then, we do different things depending on whether the user is
|
||||
// running cmd/tailscale that they built themselves (running as
|
||||
// themselves, outside the App Sandbox), or whether the user is
|
||||
// running the CLI via the GUI binary
|
||||
// (e.g. /Applications/Tailscale.app/Contents/MacOS/Tailscale <args>),
|
||||
// in which case we're running within the App Sandbox.
|
||||
//
|
||||
// If we're outside the App Sandbox:
|
||||
//
|
||||
// * then we come along here, running as the same UID, but outside
|
||||
// of the sandbox, and look for it. We can run lsof on our own processes,
|
||||
// but other users on the system can't.
|
||||
// * we parse out the localhost port number and the auth token
|
||||
// * we connect to TCP localhost:$PORT
|
||||
// * we send $TOKEN + "\n"
|
||||
// * server verifies $TOKEN, sends "#IPN\n" if okay.
|
||||
// * server is now protocol switched
|
||||
// * we return the net.Conn and the caller speaks the normal protocol
|
||||
//
|
||||
// If we're inside the App Sandbox, then TS_MACOS_CLI_SHARED_DIR has
|
||||
// been set to our shared directory. We now have to find the most
|
||||
// recent "sameuserproof" file (there should only be 1, but previous
|
||||
// versions of the macOS app didn't clean them up).
|
||||
// * the CLI looks on disk for that TCP port + auth token (see localTCPPortAndTokenDarwin)
|
||||
// * we send it upon TCP connect to prove to the Tailscale daemon that
|
||||
// we're a suitably privileged user to have access the files on disk
|
||||
// which the Network/App Extension wrote.
|
||||
func connectMacOSAppSandbox() (net.Conn, error) {
|
||||
// Are we running the Tailscale.app GUI binary as a CLI, running within the App Sandbox?
|
||||
if d := os.Getenv("TS_MACOS_CLI_SHARED_DIR"); d != "" {
|
||||
fis, err := ioutil.ReadDir(d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading TS_MACOS_CLI_SHARED_DIR: %w", err)
|
||||
}
|
||||
var best os.FileInfo
|
||||
for _, fi := range fis {
|
||||
if !strings.HasPrefix(fi.Name(), "sameuserproof-") || strings.Count(fi.Name(), "-") != 2 {
|
||||
continue
|
||||
}
|
||||
if best == nil || fi.ModTime().After(best.ModTime()) {
|
||||
best = fi
|
||||
}
|
||||
}
|
||||
if best == nil {
|
||||
return nil, fmt.Errorf("no sameuserproof token found in TS_MACOS_CLI_SHARED_DIR %q", d)
|
||||
}
|
||||
f := strings.SplitN(best.Name(), "-", 3)
|
||||
portStr, token := f[1], f[2]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port %q", portStr)
|
||||
}
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
// Otherwise, assume we're running the cmd/tailscale binary from outside the
|
||||
// App Sandbox.
|
||||
port, token, err := LocalTCPPortAndToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to find local Tailscale daemon: %w", err)
|
||||
}
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
// connectMacTCP creates an authenticated net.Conn to the local macOS Tailscale
|
||||
// daemon for used by the "IPN" JSON message bus protocol (Tailscale's original
|
||||
// local non-HTTP IPC protocol).
|
||||
func connectMacTCP(port int, token string) (net.Conn, error) {
|
||||
c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
|
||||
@@ -41,16 +41,11 @@ main() {
|
||||
# - ID: the short name of the OS (e.g. "debian", "freebsd")
|
||||
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
|
||||
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
|
||||
# - UBUNTU_CODENAME: if it exists, as in linuxmint, use instead of VERSION_CODENAME
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|pop|neon|zorin|elementary|linuxmint)
|
||||
ubuntu|pop|neon|zorin|elementary)
|
||||
OS="ubuntu"
|
||||
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
|
||||
VERSION="$UBUNTU_CODENAME"
|
||||
else
|
||||
VERSION="$VERSION_CODENAME"
|
||||
fi
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
# Third-party keyrings became the preferred method of
|
||||
# installation in Ubuntu 20.04.
|
||||
@@ -72,6 +67,35 @@ main() {
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
linuxmint)
|
||||
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
|
||||
OS="ubuntu"
|
||||
VERSION="$UBUNTU_CODENAME"
|
||||
elif [ "${DEBIAN_CODENAME:-}" != "" ]; then
|
||||
OS="debian"
|
||||
VERSION="$DEBIAN_CODENAME"
|
||||
else
|
||||
OS="ubuntu"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
fi
|
||||
PACKAGETYPE="apt"
|
||||
if [ "$VERSION_ID" -lt 5 ]; then
|
||||
APT_KEY_TYPE="legacy"
|
||||
else
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
parrot)
|
||||
OS="debian"
|
||||
PACKAGETYPE="apt"
|
||||
if [ "$VERSION_ID" -lt 5 ]; then
|
||||
VERSION="buster"
|
||||
APT_KEY_TYPE="legacy"
|
||||
else
|
||||
VERSION="bullseye"
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
raspbian)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
@@ -127,7 +151,7 @@ main() {
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
rocky)
|
||||
rocky|almalinux)
|
||||
OS="fedora"
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
@@ -474,7 +498,7 @@ main() {
|
||||
;;
|
||||
emerge)
|
||||
set -x
|
||||
$SUDO emerge net-vpn/tailscale
|
||||
$SUDO emerge --ask=n net-vpn/tailscale
|
||||
set +x
|
||||
;;
|
||||
appstore)
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
{
|
||||
pkgs ? import <nixpkgs> {},
|
||||
nixosUnstable ? import (fetchTarball https://github.com/NixOS/nixpkgs/archive/refs/heads/nixpkgs-unstable.tar.gz) { },
|
||||
tailscale-go-rev ? "5ce3ec4d89c72f2a2b6f6f5089c950d7a6a33530",
|
||||
tailscale-go-sha ? "sha256-KMOfzmikh30vEkViEkWUsOHczUifSTiRL6rhKQpHCRI=",
|
||||
tailscale-go-rev ? "710a0d861098c07540ad073bb73a42ce81bf54a8",
|
||||
tailscale-go-sha ? "sha256-hnyddxiyqMFHGwV3I4wkBcYNd56schYFi0SL5/0PnMI=",
|
||||
}:
|
||||
let
|
||||
tailscale-go = pkgs.lib.overrideDerivation nixosUnstable.go_1_18 (attrs: rec {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file contains the code for the incubator process.
|
||||
// Taiscaled launches the incubator as the same user as it was launched as.
|
||||
// The incbuator then registers a new session with the OS, sets its own UID to
|
||||
// the specified `--uid`` and then lauches the requested `--cmd`.
|
||||
// This file contains the code for the incubator process. Taiscaled
|
||||
// launches the incubator as the same user as it was launched as. The
|
||||
// incubator then registers a new session with the OS, sets its UID
|
||||
// and groups to the specified `--uid`, `--gid` and `--groups`, and
|
||||
// then lauches the requested `--cmd`.
|
||||
|
||||
//go:build linux || (darwin && !ios)
|
||||
// +build linux darwin,!ios
|
||||
@@ -13,7 +14,6 @@
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -25,10 +25,12 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/u-root/u-root/pkg/termios"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -58,12 +60,32 @@ var maybeStartLoginSession = func(logf logger.Logf, uid uint32, localUser, remot
|
||||
//
|
||||
// If ss.srv.tailscaledPath is empty, this method is equivalent to
|
||||
// exec.CommandContext.
|
||||
func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args []string) *exec.Cmd {
|
||||
if ss.srv.tailscaledPath == "" {
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
func (ss *sshSession) newIncubatorCommand() *exec.Cmd {
|
||||
var (
|
||||
name string
|
||||
args []string
|
||||
isSFTP bool
|
||||
)
|
||||
switch ss.Subsystem() {
|
||||
case "sftp":
|
||||
isSFTP = true
|
||||
case "":
|
||||
name = loginShell(ss.conn.localUser.Uid)
|
||||
if rawCmd := ss.RawCommand(); rawCmd != "" {
|
||||
args = append(args, "-c", rawCmd)
|
||||
} else {
|
||||
args = append(args, "-l") // login shell
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected subsystem: %v", ss.Subsystem()))
|
||||
}
|
||||
lu := ss.localUser
|
||||
ci := ss.connInfo
|
||||
|
||||
if ss.conn.srv.tailscaledPath == "" {
|
||||
// TODO(maisem): this doesn't work with sftp
|
||||
return exec.CommandContext(ss.ctx, name, args...)
|
||||
}
|
||||
lu := ss.conn.localUser
|
||||
ci := ss.conn.info
|
||||
remoteUser := ci.uprof.LoginName
|
||||
if len(ci.node.Tags) > 0 {
|
||||
remoteUser = strings.Join(ci.node.Tags, ",")
|
||||
@@ -73,41 +95,66 @@ func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args
|
||||
"be-child",
|
||||
"ssh",
|
||||
"--uid=" + lu.Uid,
|
||||
"--gid=" + lu.Gid,
|
||||
"--groups=" + strings.Join(ss.conn.userGroupIDs, ","),
|
||||
"--local-user=" + lu.Username,
|
||||
"--remote-user=" + remoteUser,
|
||||
"--remote-ip=" + ci.src.IP().String(),
|
||||
"--cmd=" + name,
|
||||
"--has-tty=false", // updated in-place by startWithPTY
|
||||
"--tty-name=", // updated in-place by startWithPTY
|
||||
}
|
||||
if len(args) > 0 {
|
||||
incubatorArgs = append(incubatorArgs, "--")
|
||||
incubatorArgs = append(incubatorArgs, args...)
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, ss.srv.tailscaledPath, incubatorArgs...)
|
||||
if isSFTP {
|
||||
incubatorArgs = append(incubatorArgs, "--sftp")
|
||||
} else {
|
||||
incubatorArgs = append(incubatorArgs, "--cmd="+name)
|
||||
if len(args) > 0 {
|
||||
incubatorArgs = append(incubatorArgs, "--")
|
||||
incubatorArgs = append(incubatorArgs, args...)
|
||||
}
|
||||
}
|
||||
return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
|
||||
}
|
||||
|
||||
const debugIncubator = false
|
||||
|
||||
type stdRWC struct{}
|
||||
|
||||
func (stdRWC) Read(p []byte) (n int, err error) {
|
||||
return os.Stdin.Read(p)
|
||||
}
|
||||
|
||||
func (stdRWC) Write(b []byte) (n int, err error) {
|
||||
return os.Stdout.Write(b)
|
||||
}
|
||||
|
||||
func (stdRWC) Close() error {
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// beIncubator is the entrypoint to the `tailscaled be-child ssh` subcommand.
|
||||
// It is responsible for informing the system of a new login session for the user.
|
||||
// This is sometimes necessary for mounting home directories and decrypting file
|
||||
// systems.
|
||||
//
|
||||
// Taiscaled launches the incubator as the same user as it was launched as.
|
||||
// The incbuator then registers a new session with the OS, sets its own UID to
|
||||
// the specified `--uid`` and then lauches the requested `--cmd`.
|
||||
// Tailscaled launches the incubator as the same user as it was
|
||||
// launched as. The incubator then registers a new session with the
|
||||
// OS, sets its UID and groups to the specified `--uid`, `--gid` and
|
||||
// `--groups` and then launches the requested `--cmd`.
|
||||
func beIncubator(args []string) error {
|
||||
var (
|
||||
flags = flag.NewFlagSet("", flag.ExitOnError)
|
||||
uid = flags.Uint64("uid", 0, "the uid of local-user")
|
||||
gid = flags.Int("gid", 0, "the gid of local-user")
|
||||
groups = flags.String("groups", "", "comma-separated list of gids of local-user")
|
||||
localUser = flags.String("local-user", "", "the user to run as")
|
||||
remoteUser = flags.String("remote-user", "", "the remote user/tags")
|
||||
remoteIP = flags.String("remote-ip", "", "the remote Tailscale IP")
|
||||
ttyName = flags.String("tty-name", "", "the tty name (pts/3)")
|
||||
hasTTY = flags.Bool("has-tty", false, "is the output attached to a tty")
|
||||
cmdName = flags.String("cmd", "", "the cmd to launch")
|
||||
cmdName = flags.String("cmd", "", "the cmd to launch (ignored in sftp mode)")
|
||||
sftpMode = flags.Bool("sftp", false, "run sftp server (cmd is ignored)")
|
||||
)
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
@@ -126,11 +173,28 @@ func beIncubator(args []string) error {
|
||||
// Inform the system that we are about to log someone in.
|
||||
// We can only do this if we are running as root.
|
||||
// This is best effort to still allow running on machines where
|
||||
// we don't support starting session, e.g. darwin.
|
||||
// we don't support starting sessions, e.g. darwin.
|
||||
sessionCloser, err := maybeStartLoginSession(logf, uint32(*uid), *localUser, *remoteUser, *remoteIP, *ttyName)
|
||||
if err == nil && sessionCloser != nil {
|
||||
defer sessionCloser()
|
||||
}
|
||||
var groupIDs []int
|
||||
for _, g := range strings.Split(*groups, ",") {
|
||||
gid, err := strconv.ParseInt(g, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupIDs = append(groupIDs, int(gid))
|
||||
}
|
||||
if err := syscall.Setgroups(groupIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if egid := os.Getegid(); egid != *gid {
|
||||
if err := syscall.Setgid(int(*gid)); err != nil {
|
||||
logf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if euid != *uid {
|
||||
// Switch users if required before starting the desired process.
|
||||
if err := syscall.Setuid(int(*uid)); err != nil {
|
||||
@@ -138,6 +202,15 @@ func beIncubator(args []string) error {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if *sftpMode {
|
||||
logf("handling sftp")
|
||||
|
||||
server, err := sftp.NewServer(stdRWC{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return server.Serve()
|
||||
}
|
||||
|
||||
cmd := exec.Command(*cmdName, cmdArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
@@ -165,27 +238,24 @@ func beIncubator(args []string) error {
|
||||
// The caller can wait for the process to exit by calling cmd.Wait().
|
||||
//
|
||||
// It sets ss.cmd, stdin, stdout, and stderr.
|
||||
func (ss *sshSession) launchProcess(ctx context.Context) error {
|
||||
shell := loginShell(ss.localUser.Uid)
|
||||
var args []string
|
||||
if rawCmd := ss.RawCommand(); rawCmd != "" {
|
||||
args = append(args, "-c", rawCmd)
|
||||
} else {
|
||||
args = append(args, "-l") // login shell
|
||||
func (ss *sshSession) launchProcess() error {
|
||||
ss.cmd = ss.newIncubatorCommand()
|
||||
|
||||
cmd := ss.cmd
|
||||
cmd.Dir = ss.conn.localUser.HomeDir
|
||||
cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
|
||||
for _, kv := range ss.Environ() {
|
||||
if acceptEnvPair(kv) {
|
||||
cmd.Env = append(cmd.Env, kv)
|
||||
}
|
||||
}
|
||||
|
||||
ci := ss.connInfo
|
||||
cmd := ss.newIncubatorCommand(ctx, shell, args)
|
||||
cmd.Dir = ss.localUser.HomeDir
|
||||
cmd.Env = append(cmd.Env, envForUser(ss.localUser)...)
|
||||
cmd.Env = append(cmd.Env, ss.Environ()...)
|
||||
ci := ss.conn.info
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.IP(), ci.src.Port(), ci.dst.Port()),
|
||||
fmt.Sprintf("SSH_CONNECTION=%s %d %s %d", ci.src.IP(), ci.src.Port(), ci.dst.IP(), ci.dst.Port()),
|
||||
)
|
||||
|
||||
ss.cmd = cmd
|
||||
|
||||
if ss.agentListener != nil {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_AUTH_SOCK=%s", ss.agentListener.Addr()))
|
||||
}
|
||||
@@ -217,7 +287,7 @@ func resizeWindow(f *os.File, winCh <-chan ssh.Window) {
|
||||
}
|
||||
|
||||
// opcodeShortName is a mapping of SSH opcode
|
||||
// to mnemonic names expected by the termios packaage.
|
||||
// to mnemonic names expected by the termios package.
|
||||
// These are meant to be platform independent.
|
||||
var opcodeShortName = map[uint8]string{
|
||||
gossh.VINTR: "intr",
|
||||
@@ -330,7 +400,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
|
||||
}
|
||||
k, ok := opcodeShortName[c]
|
||||
if !ok {
|
||||
ss.logf("unknown opcode: %d", c)
|
||||
ss.vlogf("unknown opcode: %d", c)
|
||||
continue
|
||||
}
|
||||
if _, ok := tios.CC[k]; ok {
|
||||
@@ -341,7 +411,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
|
||||
tios.Opts[k] = v > 0
|
||||
continue
|
||||
}
|
||||
ss.logf("unsupported opcode: %v(%d)=%v", k, c, v)
|
||||
ss.vlogf("unsupported opcode: %v(%d)=%v", k, c, v)
|
||||
}
|
||||
|
||||
// Save PTY settings.
|
||||
@@ -430,7 +500,7 @@ func loginShell(uid string) string {
|
||||
if e := os.Getenv("SHELL"); e != "" {
|
||||
return e
|
||||
}
|
||||
return "/bin/bash"
|
||||
return "/bin/sh"
|
||||
}
|
||||
|
||||
func envForUser(u *user.User) []string {
|
||||
@@ -451,3 +521,14 @@ func updateStringInSlice(ss []string, a, b string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// acceptEnvPair reports whether the environment variable key=value pair
|
||||
// should be accepted from the client. It uses the same default as OpenSSH
|
||||
// AcceptEnv.
|
||||
func acceptEnvPair(kv string) bool {
|
||||
k, _, ok := strings.Cut(kv, "=")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return k == "TERM" || k == "LANG" || strings.HasPrefix(k, "LC_")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,19 @@ package tailssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +31,7 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cibuild"
|
||||
"tailscale.com/util/lineread"
|
||||
@@ -56,14 +63,17 @@ func TestMatchRule(t *testing.T) {
|
||||
Action: someAction,
|
||||
RuleExpires: timePtr(time.Unix(100, 0)),
|
||||
},
|
||||
ci: &sshConnInfo{now: time.Unix(200, 0)},
|
||||
ci: &sshConnInfo{},
|
||||
wantErr: errRuleExpired,
|
||||
},
|
||||
{
|
||||
name: "no-principal",
|
||||
rule: &tailcfg.SSHRule{
|
||||
Action: someAction,
|
||||
},
|
||||
SSHUsers: map[string]string{
|
||||
"*": "ubuntu",
|
||||
}},
|
||||
ci: &sshConnInfo{},
|
||||
wantErr: errPrincipalMatch,
|
||||
},
|
||||
{
|
||||
@@ -168,7 +178,11 @@ func TestMatchRule(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, gotUser, err := matchRule(tt.rule, tt.ci)
|
||||
c := &conn{
|
||||
now: time.Unix(200, 0),
|
||||
info: tt.ci,
|
||||
}
|
||||
got, gotUser, err := c.matchRule(tt.rule, nil)
|
||||
if err != tt.wantErr {
|
||||
t.Errorf("err = %v; want %v", err, tt.wantErr)
|
||||
}
|
||||
@@ -205,17 +219,19 @@ func TestSSH(t *testing.T) {
|
||||
lb: lb,
|
||||
logf: logf,
|
||||
}
|
||||
ss, err := srv.newSSHServer()
|
||||
sc, err := srv.newConn()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Remove the auth checks for the test
|
||||
sc.insecureSkipTailscaleAuth = true
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ci := &sshConnInfo{
|
||||
sc.localUser = u
|
||||
sc.info = &sshConnInfo{
|
||||
sshUser: "test",
|
||||
src: netaddr.MustParseIPPort("1.2.3.4:32342"),
|
||||
dst: netaddr.MustParseIPPort("1.2.3.5:22"),
|
||||
@@ -223,10 +239,8 @@ func TestSSH(t *testing.T) {
|
||||
uprof: &tailcfg.UserProfile{},
|
||||
}
|
||||
|
||||
ss.Handler = func(s ssh.Session) {
|
||||
ss := srv.newSSHSession(s, ci, u)
|
||||
ss.action = &tailcfg.SSHAction{Accept: true}
|
||||
ss.run()
|
||||
sc.Handler = func(s ssh.Session) {
|
||||
sc.newSSHSession(s, &tailcfg.SSHAction{Accept: true}).run()
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
@@ -245,12 +259,15 @@ func TestSSH(t *testing.T) {
|
||||
}
|
||||
return
|
||||
}
|
||||
go ss.HandleConn(c)
|
||||
go sc.HandleConn(c)
|
||||
}
|
||||
}()
|
||||
|
||||
execSSH := func(args ...string) *exec.Cmd {
|
||||
cmd := exec.Command("ssh",
|
||||
"-F",
|
||||
"none",
|
||||
"-v",
|
||||
"-p", fmt.Sprint(port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"user@127.0.0.1")
|
||||
@@ -266,7 +283,7 @@ func TestSSH(t *testing.T) {
|
||||
cmd.Env = append(os.Environ(), "LOCAL_ENV=bar")
|
||||
got, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatal(err, string(got))
|
||||
}
|
||||
m := parseEnv(got)
|
||||
if got := m["USER"]; got == "" || got != u.Username {
|
||||
@@ -333,3 +350,105 @@ func parseEnv(out []byte) map[string]string {
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func TestPublicKeyFetching(t *testing.T) {
|
||||
var reqsTotal, reqsIfNoneMatchHit, reqsIfNoneMatchMiss int32
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32((&reqsTotal), 1)
|
||||
etag := fmt.Sprintf("W/%q", sha256.Sum256([]byte(r.URL.Path)))
|
||||
w.Header().Set("Etag", etag)
|
||||
if v := r.Header.Get("If-None-Match"); v != "" {
|
||||
if v == etag {
|
||||
atomic.AddInt32(&reqsIfNoneMatchHit, 1)
|
||||
w.WriteHeader(304)
|
||||
return
|
||||
}
|
||||
atomic.AddInt32(&reqsIfNoneMatchMiss, 1)
|
||||
}
|
||||
io.WriteString(w, "foo\nbar\n"+string(r.URL.Path)+"\n")
|
||||
}))
|
||||
ts.StartTLS()
|
||||
defer ts.Close()
|
||||
keys := ts.URL
|
||||
|
||||
clock := &tstest.Clock{}
|
||||
srv := &server{
|
||||
pubKeyHTTPClient: ts.Client(),
|
||||
timeNow: clock.Now,
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
got, err := srv.fetchPublicKeysURL(keys + "/alice.keys")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"foo", "bar", "/alice.keys"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
if got, want := atomic.LoadInt32(&reqsTotal), int32(1); got != want {
|
||||
t.Errorf("got %d requests; want %d", got, want)
|
||||
}
|
||||
if got, want := atomic.LoadInt32(&reqsIfNoneMatchHit), int32(0); got != want {
|
||||
t.Errorf("got %d etag hits; want %d", got, want)
|
||||
}
|
||||
clock.Advance(5 * time.Minute)
|
||||
got, err := srv.fetchPublicKeysURL(keys + "/alice.keys")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"foo", "bar", "/alice.keys"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := atomic.LoadInt32(&reqsTotal), int32(2); got != want {
|
||||
t.Errorf("got %d requests; want %d", got, want)
|
||||
}
|
||||
if got, want := atomic.LoadInt32(&reqsIfNoneMatchHit), int32(1); got != want {
|
||||
t.Errorf("got %d etag hits; want %d", got, want)
|
||||
}
|
||||
if got, want := atomic.LoadInt32(&reqsIfNoneMatchMiss), int32(0); got != want {
|
||||
t.Errorf("got %d etag misses; want %d", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExpandPublicKeyURL(t *testing.T) {
|
||||
c := &conn{
|
||||
info: &sshConnInfo{
|
||||
uprof: &tailcfg.UserProfile{
|
||||
LoginName: "bar@baz.tld",
|
||||
},
|
||||
},
|
||||
}
|
||||
if got, want := c.expandPublicKeyURL("foo"), "foo"; got != want {
|
||||
t.Errorf("basic: got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := c.expandPublicKeyURL("https://example.com/$LOGINNAME_LOCALPART.keys"), "https://example.com/bar.keys"; got != want {
|
||||
t.Errorf("localpart: got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email=bar@baz.tld"; got != want {
|
||||
t.Errorf("email: got %q; want %q", got, want)
|
||||
}
|
||||
c.info = new(sshConnInfo)
|
||||
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email="; got != want {
|
||||
t.Errorf("on empty: got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptEnvPair(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"TERM=x", true},
|
||||
{"term=x", false},
|
||||
{"TERM", false},
|
||||
{"LC_FOO=x", true},
|
||||
{"LD_PRELOAD=naah", false},
|
||||
{"TERM=screen-256color", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := acceptEnvPair(tt.in); got != tt.want {
|
||||
t.Errorf("for %q, got %v; want %v", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ type CapabilityVersion int
|
||||
// 28: 2022-03-09: client can communicate over Noise.
|
||||
// 29: 2022-03-21: MapResponse.PopBrowserURL
|
||||
// 30: 2022-03-22: client can request id tokens.
|
||||
const CurrentCapabilityVersion CapabilityVersion = 30
|
||||
// 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support
|
||||
// 32: 2022-04-17: client knows FilterRule.CapMatch
|
||||
const CurrentCapabilityVersion CapabilityVersion = 32
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -458,6 +460,7 @@ type Hostinfo struct {
|
||||
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
|
||||
OS string `json:",omitempty"` // operating system the client runs on (a version.OS value)
|
||||
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
Desktop opt.Bool `json:",omitempty"` // if a desktop was detected on Linux
|
||||
Package string `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
|
||||
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")
|
||||
Hostname string `json:",omitempty"` // name of the host the client runs on
|
||||
@@ -1050,6 +1053,18 @@ type NetPortRange struct {
|
||||
Ports PortRange
|
||||
}
|
||||
|
||||
// CapGrant grants capabilities in a FilterRule.
|
||||
type CapGrant struct {
|
||||
// Dsts are the destination IP ranges that this capabilty
|
||||
// grant matches.
|
||||
Dsts []netaddr.IPPrefix
|
||||
|
||||
// Caps are the capabilities the source IP matched by
|
||||
// FilterRule.SrcIPs are granted to the destination IP,
|
||||
// matched by Dsts.
|
||||
Caps []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// FilterRule represents one rule in a packet filter.
|
||||
//
|
||||
// A rule is logically a set of source CIDRs to match (described by
|
||||
@@ -1080,7 +1095,9 @@ type FilterRule struct {
|
||||
|
||||
// DstPorts are the port ranges to allow once a source IP
|
||||
// matches (is in the CIDR described by SrcIPs & SrcBits).
|
||||
DstPorts []NetPortRange
|
||||
//
|
||||
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
|
||||
DstPorts []NetPortRange `json:",omitempty"`
|
||||
|
||||
// IPProto are the IP protocol numbers to match.
|
||||
//
|
||||
@@ -1092,6 +1109,18 @@ type FilterRule struct {
|
||||
// Depending on the IPProto values, DstPorts may or may not be
|
||||
// used.
|
||||
IPProto []int `json:",omitempty"`
|
||||
|
||||
// CapGrant, if non-empty, are the capabilities to
|
||||
// conditionally grant to the source IP in SrcIPs.
|
||||
//
|
||||
// Think of DstPorts as "capabilities for networking" and
|
||||
// CapGrant as arbitrary application-defined capabilities
|
||||
// defined between the admin's ACLs and the application
|
||||
// doing WhoIs lookups, looking up the remote IP address's
|
||||
// application-level capabilities.
|
||||
//
|
||||
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
|
||||
CapGrant []CapGrant `json:",omitempty"`
|
||||
}
|
||||
|
||||
var FilterAllowAll = []FilterRule{
|
||||
@@ -1194,8 +1223,8 @@ type DNSRecord struct {
|
||||
|
||||
// PingRequest with no IP and Types is a request to send an HTTP request to prove the
|
||||
// long-polling client is still connected.
|
||||
// PingRequest with Types and IP, will send a ping to the IP and send a
|
||||
// POST request to the URL to prove that the ping succeeded.
|
||||
// PingRequest with Types and IP, will send a ping to the IP and send a POST
|
||||
// request containing a PingResponse to the URL containing results.
|
||||
type PingRequest struct {
|
||||
// URL is the URL to send a HEAD request to.
|
||||
// It will be a unique URL each time. No auth headers are necessary.
|
||||
@@ -1218,6 +1247,48 @@ type PingRequest struct {
|
||||
IP netaddr.IP
|
||||
}
|
||||
|
||||
// PingResponse provides result information for a TSMP or Disco PingRequest.
|
||||
// Typically populated from an ipnstate.PingResult used in `tailscale ping`.
|
||||
type PingResponse struct {
|
||||
Type string // ping type, such as TSMP or disco.
|
||||
|
||||
IP string `json:",omitempty"` // ping destination
|
||||
NodeIP string `json:",omitempty"` // Tailscale IP of node handling IP (different for subnet routers)
|
||||
NodeName string `json:",omitempty"` // DNS name base or (possibly not unique) hostname
|
||||
|
||||
// Err contains a short description of error conditions if the PingRequest
|
||||
// could not be fulfilled for some reason.
|
||||
// e.g. "100.1.2.3 is local Tailscale IP"
|
||||
Err string `json:",omitempty"`
|
||||
|
||||
// LatencySeconds reports measurement of the round-trip time of a message to
|
||||
// the requested target, if it could be determined. If LatencySeconds is
|
||||
// omitted, Err should contain information as to the cause.
|
||||
LatencySeconds float64 `json:",omitempty"`
|
||||
|
||||
// Endpoint is the ip:port if direct UDP was used.
|
||||
// It is not currently set for TSMP pings.
|
||||
Endpoint string `json:",omitempty"`
|
||||
|
||||
// DERPRegionID is non-zero DERP region ID if DERP was used.
|
||||
// It is not currently set for TSMP pings.
|
||||
DERPRegionID int `json:",omitempty"`
|
||||
|
||||
// DERPRegionCode is the three-letter region code
|
||||
// corresponding to DERPRegionID.
|
||||
// It is not currently set for TSMP pings.
|
||||
DERPRegionCode string `json:",omitempty"`
|
||||
|
||||
// PeerAPIPort is set by TSMP ping responses for peers that
|
||||
// are running a peerapi server. This is the port they're
|
||||
// running the server on.
|
||||
PeerAPIPort uint16 `json:",omitempty"`
|
||||
|
||||
// IsLocalIP is whether the ping request error is due to it being
|
||||
// a ping to the local node.
|
||||
IsLocalIP bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
type MapResponse struct {
|
||||
// KeepAlive, if set, represents an empty message just to keep
|
||||
// the connection alive. When true, all other fields except
|
||||
@@ -1388,6 +1459,10 @@ type Debug struct {
|
||||
// new attempts at UPnP connections.
|
||||
DisableUPnP opt.Bool `json:",omitempty"`
|
||||
|
||||
// DisableLogTail disables the logtail package. Once disabled it can't be
|
||||
// re-enabled for the lifetime of the process.
|
||||
DisableLogTail bool `json:",omitempty"`
|
||||
|
||||
// Exit optionally specifies that the client should os.Exit
|
||||
// with this code.
|
||||
Exit *int `json:",omitempty"`
|
||||
@@ -1507,8 +1582,19 @@ type Oauth2Token struct {
|
||||
}
|
||||
|
||||
const (
|
||||
// MapResponse.Node self capabilities.
|
||||
|
||||
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
|
||||
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
|
||||
|
||||
// Inter-node capabilities.
|
||||
|
||||
// CapabilityFileSharingSend grants the ability to receive files from a
|
||||
// node that's owned by a different user.
|
||||
CapabilityFileSharingSend = "https://tailscale.com/cap/file-send"
|
||||
// CapabilityDebugPeer grants the ability for a peer to read this node's
|
||||
// goroutines, metrics, magicsock internal state, etc.
|
||||
CapabilityDebugPeer = "https://tailscale.com/cap/debug-peer"
|
||||
)
|
||||
|
||||
// SetDNSRequest is a request to add a DNS record.
|
||||
@@ -1550,9 +1636,21 @@ type SetDNSResponse struct{}
|
||||
// SSHPolicy is the policy for how to handle incoming SSH connections
|
||||
// over Tailscale.
|
||||
type SSHPolicy struct {
|
||||
// Rules are the rules to process for an incoming SSH
|
||||
// connection. The first matching rule takes its action and
|
||||
// stops processing further rules.
|
||||
// Rules are the rules to process for an incoming SSH connection. The first
|
||||
// matching rule takes its action and stops processing further rules.
|
||||
//
|
||||
// When an incoming connection first starts, all rules are evaluated in
|
||||
// "none" auth mode, where the client hasn't even been asked to send a
|
||||
// public key. All SSHRule.Principals requiring a public key won't match. If
|
||||
// a rule matches on the first pass and its Action is reject, the
|
||||
// authentication fails with that action's rejection message, if any.
|
||||
//
|
||||
// If the first pass rule evaluation matches nothing without matching an
|
||||
// Action with Reject set, the rules are considered to see whether public
|
||||
// keys might still result in a match. If not, "none" auth is terminated
|
||||
// before proceeding to public key mode. If so, the client is asked to try
|
||||
// public key authentication and the rules are evaluated again for each of
|
||||
// the client's present keys.
|
||||
Rules []*SSHRule `json:"rules"`
|
||||
}
|
||||
|
||||
@@ -1593,16 +1691,26 @@ type SSHRule struct {
|
||||
}
|
||||
|
||||
// SSHPrincipal is either a particular node or a user on any node.
|
||||
// Any matching field causes a match.
|
||||
type SSHPrincipal struct {
|
||||
// Matching any one of the following four field causes a match.
|
||||
// It must also match Certs, if non-empty.
|
||||
|
||||
Node StableNodeID `json:"node,omitempty"`
|
||||
NodeIP string `json:"nodeIP,omitempty"`
|
||||
UserLogin string `json:"userLogin,omitempty"` // email-ish: foo@example.com, bar@github
|
||||
|
||||
// Any, if true, matches any user.
|
||||
Any bool `json:"any,omitempty"`
|
||||
|
||||
Any bool `json:"any,omitempty"` // if true, match any connection
|
||||
// TODO(bradfitz): add StableUserID, once that exists
|
||||
|
||||
// PubKeys, if non-empty, means that this SSHPrincipal only
|
||||
// matches if one of these public keys is presented by the user.
|
||||
//
|
||||
// As a special case, if len(PubKeys) == 1 and PubKeys[0] starts
|
||||
// with "https://", then it's fetched (like https://github.com/username.keys).
|
||||
// In that case, the following variable expansions are also supported
|
||||
// in the URL:
|
||||
// * $LOGINNAME_EMAIL ("foo@bar.com" or "foo@github")
|
||||
// * $LOGINNAME_LOCALPART (the "foo" from either of the above)
|
||||
PubKeys []string `json:"pubKeys,omitempty"`
|
||||
}
|
||||
|
||||
// SSHAction is how to handle an incoming connection.
|
||||
@@ -1622,9 +1730,9 @@ type SSHAction struct {
|
||||
// without further prompts.
|
||||
Accept bool `json:"accept,omitempty"`
|
||||
|
||||
// SesssionDuration, if non-zero, is how long the session can stay open
|
||||
// SessionDuration, if non-zero, is how long the session can stay open
|
||||
// before being forcefully terminated.
|
||||
SesssionDuration time.Duration `json:"sessionDuration,omitempty"`
|
||||
SessionDuration time.Duration `json:"sessionDuration,omitempty"`
|
||||
|
||||
// AllowAgentForwarding, if true, allows accepted connections to forward
|
||||
// the ssh agent if requested.
|
||||
|
||||
@@ -117,6 +117,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
|
||||
BackendLogID string
|
||||
OS string
|
||||
OSVersion string
|
||||
Desktop opt.Bool
|
||||
Package string
|
||||
DeviceModel string
|
||||
Hostname string
|
||||
|
||||
@@ -28,7 +28,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestHostinfoEqual(t *testing.T) {
|
||||
hiHandles := []string{
|
||||
"IPNVersion", "FrontendLogID", "BackendLogID",
|
||||
"OS", "OSVersion", "Package", "DeviceModel", "Hostname",
|
||||
"OS", "OSVersion", "Desktop", "Package", "DeviceModel", "Hostname",
|
||||
"ShieldsUp", "ShareeNode",
|
||||
"GoArch",
|
||||
"RoutableIPs", "RequestTags",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ssh_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
@@ -27,10 +28,19 @@ func ExampleNoPty() {
|
||||
|
||||
func ExamplePublicKeyAuth() {
|
||||
ssh.ListenAndServe(":2222", nil,
|
||||
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
data, _ := ioutil.ReadFile("/path/to/allowed/key.pub")
|
||||
allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data)
|
||||
return ssh.KeysEqual(key, allowed)
|
||||
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error {
|
||||
data, err := ioutil.ReadFile("/path/to/allowed/key.pub")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allowed, _, _, _, err := ssh.ParseAuthorizedKey(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ssh.KeysEqual(key, allowed) {
|
||||
return errors.New("some error")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ type Server struct {
|
||||
HostSigners []Signer // private keys for the host key, must have at least one
|
||||
Version string // server version to be sent before the initial handshake
|
||||
|
||||
NoClientAuthCallback func(gossh.ConnMetadata) (*gossh.Permissions, error)
|
||||
|
||||
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
|
||||
PasswordHandler PasswordHandler // password authentication handler
|
||||
PublicKeyHandler PublicKeyHandler // public key authentication handler
|
||||
@@ -131,10 +129,6 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
|
||||
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil {
|
||||
config.NoClientAuth = true
|
||||
}
|
||||
if srv.NoClientAuthCallback != nil {
|
||||
config.NoClientAuth = true
|
||||
config.NoClientAuthCallback = srv.NoClientAuthCallback
|
||||
}
|
||||
if srv.Version != "" {
|
||||
config.ServerVersion = "SSH-2.0-" + srv.Version
|
||||
}
|
||||
@@ -150,8 +144,8 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
|
||||
if srv.PublicKeyHandler != nil {
|
||||
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||
applyConnMetadata(ctx, conn)
|
||||
if ok := srv.PublicKeyHandler(ctx, key); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
if err := srv.PublicKeyHandler(ctx, key); err != nil {
|
||||
return ctx.Permissions().Permissions, err
|
||||
}
|
||||
ctx.SetValue(ContextKeyPublicKey, key)
|
||||
return ctx.Permissions().Permissions, nil
|
||||
|
||||
@@ -36,7 +36,7 @@ type Option func(*Server) error
|
||||
type Handler func(Session)
|
||||
|
||||
// PublicKeyHandler is a callback for performing public key authentication.
|
||||
type PublicKeyHandler func(ctx Context, key PublicKey) bool
|
||||
type PublicKeyHandler func(ctx Context, key PublicKey) error
|
||||
|
||||
// PasswordHandler is a callback for performing password authentication.
|
||||
type PasswordHandler func(ctx Context, password string) bool
|
||||
|
||||
@@ -105,6 +105,7 @@ func (s *Server) Close() error {
|
||||
s.shutdownCancel()
|
||||
s.lb.Shutdown()
|
||||
s.linkMon.Close()
|
||||
s.dialer.Close()
|
||||
s.localAPIListener.Close()
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -137,8 +138,9 @@ func (s *Server) start() error {
|
||||
}
|
||||
|
||||
s.rootPath = s.Dir
|
||||
if s.Store != nil && !s.Ephemeral {
|
||||
if _, ok := s.Store.(*mem.Store); !ok {
|
||||
if s.Store != nil {
|
||||
_, isMemStore := s.Store.(*mem.Store)
|
||||
if isMemStore && !s.Ephemeral {
|
||||
return fmt.Errorf("in-memory store is only supported for Ephemeral nodes")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/safesocket"
|
||||
_ "tailscale.com/ssh/tailssh"
|
||||
_ "tailscale.com/tailcfg"
|
||||
_ "tailscale.com/tsweb"
|
||||
_ "tailscale.com/types/flagtype"
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/safesocket"
|
||||
_ "tailscale.com/ssh/tailssh"
|
||||
_ "tailscale.com/tailcfg"
|
||||
_ "tailscale.com/tsweb"
|
||||
_ "tailscale.com/types/flagtype"
|
||||
|
||||
62
tstest/iosdeps/iosdeps.go
Normal file
62
tstest/iosdeps/iosdeps.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package iosdeps is a just a list of the packages we import on iOS, to let us
|
||||
// test that our transitive closure of dependencies on iOS doesn't accidentally
|
||||
// grow too large, as we've historically been memory constrained there.
|
||||
package iosdeps
|
||||
|
||||
import (
|
||||
_ "bufio"
|
||||
_ "bytes"
|
||||
_ "context"
|
||||
_ "crypto/rand"
|
||||
_ "crypto/sha256"
|
||||
_ "encoding/json"
|
||||
_ "errors"
|
||||
_ "fmt"
|
||||
_ "io"
|
||||
_ "io/fs"
|
||||
_ "io/ioutil"
|
||||
_ "log"
|
||||
_ "math"
|
||||
_ "net"
|
||||
_ "net/http"
|
||||
_ "os"
|
||||
_ "os/signal"
|
||||
_ "path/filepath"
|
||||
_ "runtime"
|
||||
_ "runtime/debug"
|
||||
_ "strings"
|
||||
_ "sync"
|
||||
_ "sync/atomic"
|
||||
_ "syscall"
|
||||
_ "time"
|
||||
_ "unsafe"
|
||||
|
||||
_ "go4.org/mem"
|
||||
_ "golang.org/x/sys/unix"
|
||||
_ "golang.zx2c4.com/wireguard/device"
|
||||
_ "golang.zx2c4.com/wireguard/tun"
|
||||
_ "inet.af/netaddr"
|
||||
_ "tailscale.com/hostinfo"
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/localapi"
|
||||
_ "tailscale.com/log/logheap"
|
||||
_ "tailscale.com/logtail"
|
||||
_ "tailscale.com/logtail/filch"
|
||||
_ "tailscale.com/net/dns"
|
||||
_ "tailscale.com/net/tsdial"
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/tempfork/pprof"
|
||||
_ "tailscale.com/types/empty"
|
||||
_ "tailscale.com/types/logger"
|
||||
_ "tailscale.com/util/clientmetric"
|
||||
_ "tailscale.com/util/dnsname"
|
||||
_ "tailscale.com/version"
|
||||
_ "tailscale.com/wgengine"
|
||||
_ "tailscale.com/wgengine/router"
|
||||
)
|
||||
42
tstest/iosdeps/iosdeps_test.go
Normal file
42
tstest/iosdeps/iosdeps_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// No need to run this on Windows where CI's slow enough. Then we don't need to
|
||||
// worry about "go.exe" etc.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package iosdeps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "list", "-json", ".")
|
||||
cmd.Env = append(os.Environ(), "GOOS=ios", "GOARCH=arm64")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var res struct {
|
||||
Deps []string
|
||||
}
|
||||
if err := json.Unmarshal(out, &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, dep := range res.Deps {
|
||||
switch dep {
|
||||
case "regexp", "regexp/syntax", "text/template", "html/template":
|
||||
t.Errorf("package %q is not allowed as a dependency on iOS", dep)
|
||||
}
|
||||
}
|
||||
t.Logf("got %d dependencies", len(res.Deps))
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import "inet.af/netaddr"
|
||||
// Resolver is the configuration for one DNS resolver.
|
||||
type Resolver struct {
|
||||
// Addr is the address of the DNS resolver, one of:
|
||||
// - A plain IP address for a "classic" UDP+TCP DNS resolver
|
||||
// - A plain IP address for a "classic" UDP+TCP DNS resolver.
|
||||
// This is the common format as sent by the control plane.
|
||||
// - An IP:port, for tests.
|
||||
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
|
||||
// - [TODO] "https://resolver.com/query-tmpl" for DNS over HTTPS
|
||||
Addr string `json:",omitempty"`
|
||||
@@ -26,7 +28,20 @@ type Resolver struct {
|
||||
BootstrapResolution []netaddr.IP `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ResolverFromIP defines a Resolver for ip on port 53.
|
||||
func ResolverFromIP(ip netaddr.IP) Resolver {
|
||||
return Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()}
|
||||
// IPPort returns r.Addr as an IP address and port if either
|
||||
// r.Addr is an IP address (the common case) or if r.Addr
|
||||
// is an IP:port (as done in tests).
|
||||
func (r *Resolver) IPPort() (ipp netaddr.IPPort, ok bool) {
|
||||
if r.Addr == "" || r.Addr[0] == 'h' || r.Addr[0] == 't' {
|
||||
// Fast path to avoid ParseIP error allocation for obviously not IP
|
||||
// cases.
|
||||
return
|
||||
}
|
||||
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
|
||||
return netaddr.IPPortFrom(ip, 53), true
|
||||
}
|
||||
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil {
|
||||
return ipp, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
53
util/mak/mak.go
Normal file
53
util/mak/mak.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package mak helps make maps. It contains generic helpers to make/assign
|
||||
// things, notably to maps, but also slices.
|
||||
package mak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Set populates an entry in a map, making the map if necessary.
|
||||
//
|
||||
// That is, it assigns (*m)[k] = v, making *m if it was nil.
|
||||
func Set[K comparable, V any, T ~map[K]V](m *T, k K, v V) {
|
||||
if *m == nil {
|
||||
*m = make(map[K]V)
|
||||
}
|
||||
(*m)[k] = v
|
||||
}
|
||||
|
||||
// NonNil takes a pointer to a Go data structure
|
||||
// (currently only a slice or a map) and makes sure it's non-nil for
|
||||
// JSON serialization. (In particular, JavaScript clients usually want
|
||||
// the field to be defined after they decode the JSON.)
|
||||
// MakeNonNil takes a pointer to a Go data structure
|
||||
// (currently only a slice or a map) and makes sure it's non-nil for
|
||||
// JSON serialization. (In particular, JavaScript clients usually want
|
||||
// the field to be defined after they decode the JSON.)
|
||||
func NonNil(ptr interface{}) {
|
||||
if ptr == nil {
|
||||
panic("nil interface")
|
||||
}
|
||||
rv := reflect.ValueOf(ptr)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
panic(fmt.Sprintf("kind %v, not Ptr", rv.Kind()))
|
||||
}
|
||||
if rv.Pointer() == 0 {
|
||||
panic("nil pointer")
|
||||
}
|
||||
rv = rv.Elem()
|
||||
if rv.Pointer() != 0 {
|
||||
return
|
||||
}
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Slice:
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
|
||||
case reflect.Map:
|
||||
rv.Set(reflect.MakeMap(rv.Type()))
|
||||
}
|
||||
}
|
||||
71
util/mak/mak_test.go
Normal file
71
util/mak/mak_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package mak contains code to help make things.
|
||||
package mak
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type M map[string]int
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
t.Run("unnamed", func(t *testing.T) {
|
||||
var m map[string]int
|
||||
Set(&m, "foo", 42)
|
||||
Set(&m, "bar", 1)
|
||||
Set(&m, "bar", 2)
|
||||
want := map[string]int{
|
||||
"foo": 42,
|
||||
"bar": 2,
|
||||
}
|
||||
if got := m; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("named", func(t *testing.T) {
|
||||
var m M
|
||||
Set(&m, "foo", 1)
|
||||
Set(&m, "bar", 1)
|
||||
Set(&m, "bar", 2)
|
||||
want := M{
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
}
|
||||
if got := m; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonNil(t *testing.T) {
|
||||
var s []string
|
||||
NonNil(&s)
|
||||
if len(s) != 0 {
|
||||
t.Errorf("slice len = %d; want 0", len(s))
|
||||
}
|
||||
if s == nil {
|
||||
t.Error("slice still nil")
|
||||
}
|
||||
|
||||
s = append(s, "foo")
|
||||
NonNil(&s)
|
||||
if len(s) != 1 {
|
||||
t.Errorf("len = %d; want 1", len(s))
|
||||
}
|
||||
if s[0] != "foo" {
|
||||
t.Errorf("value = %q; want foo", s)
|
||||
}
|
||||
|
||||
var m map[string]string
|
||||
NonNil(&m)
|
||||
if len(m) != 0 {
|
||||
t.Errorf("map len = %d; want 0", len(s))
|
||||
}
|
||||
if m == nil {
|
||||
t.Error("map still nil")
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,12 @@ type Filter struct {
|
||||
// destination within local, regardless of the policy filter
|
||||
// below.
|
||||
local *netaddr.IPSet
|
||||
|
||||
// logIPs is the set of IPs that are allowed to appear in flow
|
||||
// logs. If a packet is to or from an IP not in logIPs, it will
|
||||
// never be logged.
|
||||
logIPs *netaddr.IPSet
|
||||
|
||||
// matches4 and matches6 are lists of match->action rules
|
||||
// applied to all packets arriving over tailscale
|
||||
// tunnels. Matches are checked in order, and processing stops
|
||||
@@ -38,6 +40,11 @@ type Filter struct {
|
||||
// match is to drop the packet.
|
||||
matches4 matches
|
||||
matches6 matches
|
||||
|
||||
// cap4 and cap6 are the subsets of the matches that are about
|
||||
// capability grants, partitioned by source IP address family.
|
||||
cap4, cap6 matches
|
||||
|
||||
// state is the connection tracking state attached to this
|
||||
// filter. It is used to allow incoming traffic that is a response
|
||||
// to an outbound connection that this node made, even if those
|
||||
@@ -174,6 +181,8 @@ func New(matches []Match, localNets *netaddr.IPSet, logIPs *netaddr.IPSet, share
|
||||
logf: logf,
|
||||
matches4: matchesFamily(matches, netaddr.IP.Is4),
|
||||
matches6: matchesFamily(matches, netaddr.IP.Is6),
|
||||
cap4: capMatchesFunc(matches, netaddr.IP.Is4),
|
||||
cap6: capMatchesFunc(matches, netaddr.IP.Is6),
|
||||
local: localNets,
|
||||
logIPs: logIPs,
|
||||
state: state,
|
||||
@@ -205,6 +214,27 @@ func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches {
|
||||
return ret
|
||||
}
|
||||
|
||||
// capMatchesFunc returns a copy of the subset of ms for which keep(srcNet.IP)
|
||||
// and the match is a capability grant.
|
||||
func capMatchesFunc(ms matches, keep func(netaddr.IP) bool) matches {
|
||||
var ret matches
|
||||
for _, m := range ms {
|
||||
if len(m.Caps) == 0 {
|
||||
continue
|
||||
}
|
||||
retm := Match{Caps: m.Caps}
|
||||
for _, src := range m.Srcs {
|
||||
if keep(src.IP()) {
|
||||
retm.Srcs = append(retm.Srcs, src)
|
||||
}
|
||||
}
|
||||
if len(retm.Srcs) > 0 {
|
||||
ret = append(ret, retm)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func maybeHexdump(flag RunFlags, b []byte) string {
|
||||
if flag == 0 {
|
||||
return ""
|
||||
@@ -291,6 +321,30 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
|
||||
return f.RunIn(pkt, 0)
|
||||
}
|
||||
|
||||
// AppendCaps appends to base the capabilities that srcIP has talking
|
||||
// to dstIP.
|
||||
func (f *Filter) AppendCaps(base []string, srcIP, dstIP netaddr.IP) []string {
|
||||
ret := base
|
||||
var mm matches
|
||||
switch {
|
||||
case srcIP.Is4():
|
||||
mm = f.cap4
|
||||
case srcIP.Is6():
|
||||
mm = f.cap6
|
||||
}
|
||||
for _, m := range mm {
|
||||
if !ipInList(srcIP, m.Srcs) {
|
||||
continue
|
||||
}
|
||||
for _, cm := range m.Caps {
|
||||
if cm.Cap != "" && cm.Dst.Contains(dstIP) {
|
||||
ret = append(ret, cm.Cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ShieldsUp reports whether this is a "shields up" (block everything
|
||||
// incoming) filter.
|
||||
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
|
||||
|
||||
@@ -872,3 +872,83 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaps(t *testing.T) {
|
||||
mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
CapGrant: []tailcfg.CapGrant{{
|
||||
Dsts: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
},
|
||||
Caps: []string{"is_ipv4"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
CapGrant: []tailcfg.CapGrant{{
|
||||
Dsts: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
Caps: []string{"is_ipv6"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.199.0.0/16"},
|
||||
CapGrant: []tailcfg.CapGrant{{
|
||||
Dsts: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.200.0.0/16"),
|
||||
},
|
||||
Caps: []string{"some_super_admin"},
|
||||
}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filt := New(mm, nil, nil, nil, t.Logf)
|
||||
tests := []struct {
|
||||
name string
|
||||
src, dst string // IP
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "v4",
|
||||
src: "1.2.3.4",
|
||||
dst: "2.4.5.5",
|
||||
want: []string{"is_ipv4"},
|
||||
},
|
||||
{
|
||||
name: "v6",
|
||||
src: "1::1",
|
||||
dst: "2::2",
|
||||
want: []string{"is_ipv6"},
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
src: "100.199.1.2",
|
||||
dst: "100.200.3.4",
|
||||
want: []string{"is_ipv4", "some_super_admin"},
|
||||
},
|
||||
{
|
||||
name: "not_admin_bad_src",
|
||||
src: "100.198.1.2", // 198, not 199
|
||||
dst: "100.200.3.4",
|
||||
want: []string{"is_ipv4"},
|
||||
},
|
||||
{
|
||||
name: "not_admin_bad_dst",
|
||||
src: "100.199.1.2",
|
||||
dst: "100.201.3.4", // 201, not 200
|
||||
want: []string{"is_ipv4"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := filt.AppendCaps(nil, netaddr.MustParseIP(tt.src), netaddr.MustParseIP(tt.dst))
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,24 @@ func (npr NetPortRange) String() string {
|
||||
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
|
||||
}
|
||||
|
||||
// CapMatch is a capability grant match predicate.
|
||||
type CapMatch struct {
|
||||
// Dst is the IP prefix that the destination IP address matches against
|
||||
// to get the capability.
|
||||
Dst netaddr.IPPrefix
|
||||
|
||||
// Cap is the capability that's granted if the destination IP addresses
|
||||
// matches Dst.
|
||||
Cap string
|
||||
}
|
||||
|
||||
// Match matches packets from any IP address in Srcs to any ip:port in
|
||||
// Dsts.
|
||||
type Match struct {
|
||||
IPProto []ipproto.Proto // required set (no default value at this layer)
|
||||
Dsts []NetPortRange
|
||||
Srcs []netaddr.IPPrefix
|
||||
Dsts []NetPortRange // optional, if Srcs match
|
||||
Caps []CapMatch // optional, if Srcs match
|
||||
}
|
||||
|
||||
func (m Match) String() string {
|
||||
|
||||
@@ -21,14 +21,16 @@ func (src *Match) Clone() *Match {
|
||||
dst := new(Match)
|
||||
*dst = *src
|
||||
dst.IPProto = append(src.IPProto[:0:0], src.IPProto...)
|
||||
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
||||
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
|
||||
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
||||
dst.Caps = append(src.Caps[:0:0], src.Caps...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _MatchCloneNeedsRegeneration = Match(struct {
|
||||
IPProto []ipproto.Proto
|
||||
Dsts []NetPortRange
|
||||
Srcs []netaddr.IPPrefix
|
||||
Dsts []NetPortRange
|
||||
Caps []CapMatch
|
||||
}{})
|
||||
|
||||
@@ -70,6 +70,16 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, cm := range r.CapGrant {
|
||||
for _, dstNet := range cm.Dsts {
|
||||
for _, cap := range cm.Caps {
|
||||
m.Caps = append(m.Caps, CapMatch{
|
||||
Dst: dstNet,
|
||||
Cap: cap,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mm = append(mm, m)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/netconv"
|
||||
"tailscale.com/util/uniq"
|
||||
"tailscale.com/version"
|
||||
@@ -438,11 +439,7 @@ func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, derpID int, dc *derphttp
|
||||
func (c *Conn) addDerpPeerRoute(peer key.NodePublic, derpID int, dc *derphttp.Client) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.derpRoute == nil {
|
||||
c.derpRoute = make(map[key.NodePublic]derpRoute)
|
||||
}
|
||||
r := derpRoute{derpID, dc}
|
||||
c.derpRoute[peer] = r
|
||||
mak.Set(&c.derpRoute, peer, derpRoute{derpID, dc})
|
||||
}
|
||||
|
||||
// DerpMagicIP is a fake WireGuard endpoint IP address that means
|
||||
@@ -606,6 +603,7 @@ func (c *Conn) stopPeriodicReSTUNTimerLocked() {
|
||||
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) updateEndpoints(why string) {
|
||||
metricUpdateEndpoints.Add(1)
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -1050,8 +1048,8 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
already := make(map[netaddr.IPPort]tailcfg.EndpointType) // endpoint -> how it was found
|
||||
var eps []tailcfg.Endpoint // unique endpoints
|
||||
var already map[netaddr.IPPort]tailcfg.EndpointType // endpoint -> how it was found
|
||||
var eps []tailcfg.Endpoint // unique endpoints
|
||||
|
||||
ipp := func(s string) (ipp netaddr.IPPort) {
|
||||
ipp, _ = netaddr.ParseIPPort(s)
|
||||
@@ -1062,7 +1060,7 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
|
||||
return
|
||||
}
|
||||
if _, ok := already[ipp]; !ok {
|
||||
already[ipp] = et
|
||||
mak.Set(&already, ipp, et)
|
||||
eps = append(eps, tailcfg.Endpoint{Addr: ipp, Type: et})
|
||||
}
|
||||
}
|
||||
@@ -2771,6 +2769,7 @@ func (c *Conn) ReSTUN(why string) {
|
||||
// raced with a shutdown.
|
||||
return
|
||||
}
|
||||
metricReSTUNCalls.Add(1)
|
||||
|
||||
// If the user stopped the app, stop doing work. (When the
|
||||
// user stops Tailscale via the GUI apps, ipn/local.go
|
||||
@@ -2920,6 +2919,7 @@ func (c *Conn) rebind(curPortFate currentPortFate) error {
|
||||
// Rebind closes and re-binds the UDP sockets and resets the DERP connection.
|
||||
// It should be followed by a call to ReSTUN.
|
||||
func (c *Conn) Rebind() {
|
||||
metricRebindCalls.Add(1)
|
||||
if err := c.rebind(keepCurrentPort); err != nil {
|
||||
c.logf("%w", err)
|
||||
return
|
||||
@@ -3957,9 +3957,6 @@ func (de *endpoint) handleCallMeMaybe(m *disco.CallMeMaybe) {
|
||||
for ep := range de.isCallMeMaybeEP {
|
||||
de.isCallMeMaybeEP[ep] = false // mark for deletion
|
||||
}
|
||||
if de.isCallMeMaybeEP == nil {
|
||||
de.isCallMeMaybeEP = map[netaddr.IPPort]bool{}
|
||||
}
|
||||
var newEPs []netaddr.IPPort
|
||||
for _, ep := range m.MyNumber {
|
||||
if ep.IP().Is6() && ep.IP().IsLinkLocalUnicast() {
|
||||
@@ -3968,7 +3965,7 @@ func (de *endpoint) handleCallMeMaybe(m *disco.CallMeMaybe) {
|
||||
// for these.
|
||||
continue
|
||||
}
|
||||
de.isCallMeMaybeEP[ep] = true
|
||||
mak.Set(&de.isCallMeMaybeEP, ep, true)
|
||||
if es, ok := de.endpointState[ep]; ok {
|
||||
es.callMeMaybeTime = now
|
||||
} else {
|
||||
@@ -4144,6 +4141,10 @@ var (
|
||||
metricNumPeers = clientmetric.NewGauge("magicsock_netmap_num_peers")
|
||||
metricNumDERPConns = clientmetric.NewGauge("magicsock_num_derp_conns")
|
||||
|
||||
metricRebindCalls = clientmetric.NewCounter("magicsock_rebind_calls")
|
||||
metricReSTUNCalls = clientmetric.NewCounter("magicsock_restun_calls")
|
||||
metricUpdateEndpoints = clientmetric.NewCounter("magicsock_update_endpoints")
|
||||
|
||||
// Sends (data or disco)
|
||||
metricSendDERPQueued = clientmetric.NewCounter("magicsock_send_derp_queued")
|
||||
metricSendDERPErrorChan = clientmetric.NewCounter("magicsock_send_derp_error_chan")
|
||||
|
||||
@@ -42,6 +42,10 @@ type osMon interface {
|
||||
// until the osMon is closed. After a Close, the returned
|
||||
// error is ignored.
|
||||
Receive() (message, error)
|
||||
|
||||
// IsInterestingInterface reports whether the provided interface should
|
||||
// be considered for network change events.
|
||||
IsInterestingInterface(iface string) bool
|
||||
}
|
||||
|
||||
// ChangeFunc is a callback function that's called when the network
|
||||
@@ -185,13 +189,7 @@ func (m *Mon) Start() {
|
||||
}
|
||||
m.started = true
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
// For battery reasons, and because these platforms
|
||||
// don't really sleep in the same way, don't poll
|
||||
// for the wall time to detect for wake-for-sleep
|
||||
// walltime jumps.
|
||||
default:
|
||||
if shouldMonitorTimeJump {
|
||||
m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime)
|
||||
}
|
||||
|
||||
@@ -288,6 +286,13 @@ func (m *Mon) notifyRuleDeleted(rdm ipRuleDeletedMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// isInterestingInterface reports whether the provided interface should be
|
||||
// considered when checking for network state changes.
|
||||
// The ips parameter should be the IPs of the provided interface.
|
||||
func (m *Mon) isInterestingInterface(i interfaces.Interface, ips []netaddr.IPPrefix) bool {
|
||||
return m.om.IsInterestingInterface(i.Name) && interfaces.UseInterestingInterfaces(i, ips)
|
||||
}
|
||||
|
||||
// debounce calls the callback function with a delay between events
|
||||
// and exits when a stop is issued.
|
||||
func (m *Mon) debounce() {
|
||||
@@ -304,27 +309,26 @@ func (m *Mon) debounce() {
|
||||
} else {
|
||||
m.mu.Lock()
|
||||
|
||||
// See if we have a queued or new time jump signal.
|
||||
m.checkWallTimeAdvanceLocked()
|
||||
timeJumped := m.timeJumped
|
||||
if timeJumped {
|
||||
m.logf("time jumped (probably wake from sleep); synthesizing major change event")
|
||||
}
|
||||
|
||||
oldState := m.ifState
|
||||
ifChanged := !curState.EqualFiltered(oldState, interfaces.UseInterestingInterfaces, interfaces.UseInterestingIPs)
|
||||
if ifChanged {
|
||||
changed := !curState.EqualFiltered(oldState, m.isInterestingInterface, interfaces.UseInterestingIPs)
|
||||
if changed {
|
||||
m.gwValid = false
|
||||
m.ifState = curState
|
||||
|
||||
if s1, s2 := oldState.String(), curState.String(); s1 == s2 {
|
||||
m.logf("[unexpected] network state changed, but stringification didn't: %v\nold: %s\nnew: %s\n", s1,
|
||||
jsonSummary(oldState), jsonSummary(curState))
|
||||
m.logf("[unexpected] network state changed, but stringification didn't: %v", s1)
|
||||
m.logf("[unexpected] old: %s", jsonSummary(oldState))
|
||||
m.logf("[unexpected] new: %s", jsonSummary(curState))
|
||||
}
|
||||
}
|
||||
changed := ifChanged || timeJumped
|
||||
if changed {
|
||||
m.timeJumped = false
|
||||
// See if we have a queued or new time jump signal.
|
||||
if shouldMonitorTimeJump && m.checkWallTimeAdvanceLocked() {
|
||||
m.resetTimeJumpedLocked()
|
||||
if !changed {
|
||||
// Only log if it wasn't an interesting change.
|
||||
m.logf("time jumped (probably wake from sleep); synthesizing major change event")
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
for _, cb := range m.cbs {
|
||||
go cb(changed, m.ifState)
|
||||
@@ -360,22 +364,37 @@ func (m *Mon) pollWallTime() {
|
||||
if m.closed {
|
||||
return
|
||||
}
|
||||
m.checkWallTimeAdvanceLocked()
|
||||
if m.timeJumped {
|
||||
if m.checkWallTimeAdvanceLocked() {
|
||||
m.InjectEvent()
|
||||
}
|
||||
m.wallTimer.Reset(pollWallTimeInterval)
|
||||
}
|
||||
|
||||
// checkWallTimeAdvanceLocked updates m.timeJumped, if wall time jumped
|
||||
// more than 150% of pollWallTimeInterval, indicating we probably just
|
||||
// came out of sleep.
|
||||
func (m *Mon) checkWallTimeAdvanceLocked() {
|
||||
// shouldMonitorTimeJump is whether we keep a regular periodic timer running in
|
||||
// the background watching for jumps in wall time.
|
||||
//
|
||||
// We don't do this on mobile platforms for battery reasons, and because these
|
||||
// platforms don't really sleep in the same way.
|
||||
const shouldMonitorTimeJump = runtime.GOOS != "android" && runtime.GOOS != "ios"
|
||||
|
||||
// checkWallTimeAdvanceLocked reports whether wall time jumped more than 150% of
|
||||
// pollWallTimeInterval, indicating we probably just came out of sleep. Once a
|
||||
// time jump is detected it must be reset by calling resetTimeJumpedLocked.
|
||||
func (m *Mon) checkWallTimeAdvanceLocked() bool {
|
||||
if !shouldMonitorTimeJump {
|
||||
panic("unreachable") // if callers are correct
|
||||
}
|
||||
now := wallTime()
|
||||
if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 {
|
||||
m.timeJumped = true
|
||||
m.timeJumped = true // it is reset by debounce.
|
||||
}
|
||||
m.lastWall = now
|
||||
return m.timeJumped
|
||||
}
|
||||
|
||||
// resetTimeJumpedLocked consumes the signal set by checkWallTimeAdvanceLocked.
|
||||
func (m *Mon) resetTimeJumpedLocked() {
|
||||
m.timeJumped = false
|
||||
}
|
||||
|
||||
type ipRuleDeletedMessage struct {
|
||||
|
||||
@@ -112,11 +112,19 @@ func addrType(addrs []route.Addr, rtaxType int) route.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *darwinRouteMon) IsInterestingInterface(iface string) bool {
|
||||
baseName := strings.TrimRight(iface, "0123456789")
|
||||
switch baseName {
|
||||
// TODO(maisem): figure out what this list should actually be.
|
||||
case "llw", "awdl", "ipsec":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *darwinRouteMon) skipInterfaceAddrMessage(msg *route.InterfaceAddrMessage) bool {
|
||||
if la, ok := addrType(msg.Addrs, unix.RTAX_IFP).(*route.LinkAddr); ok {
|
||||
baseName := strings.TrimRight(la.Name, "0123456789")
|
||||
switch baseName {
|
||||
case "llw", "awdl", "pdp_ip", "ipsec":
|
||||
if !m.IsInterestingInterface(la.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ func newOSMon(logf logger.Logf, m *Mon) (osMon, error) {
|
||||
return &devdConn{conn}, nil
|
||||
}
|
||||
|
||||
func (c *devdConn) IsInterestingInterface(iface string) bool { return true }
|
||||
|
||||
func (c *devdConn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ func newOSMon(logf logger.Logf, m *Mon) (osMon, error) {
|
||||
return &nlConn{logf: logf, conn: conn, addrCache: make(map[uint32]map[netaddr.IP]bool)}, nil
|
||||
}
|
||||
|
||||
func (c *nlConn) IsInterestingInterface(iface string) bool { return true }
|
||||
|
||||
func (c *nlConn) Close() error { return c.conn.Close() }
|
||||
|
||||
func (c *nlConn) Receive() (message, error) {
|
||||
|
||||
@@ -72,6 +72,8 @@ func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *winMon) IsInterestingInterface(iface string) bool { return true }
|
||||
|
||||
func (m *winMon) Close() (ret error) {
|
||||
m.cancel()
|
||||
m.noDeadlockTicker.Stop()
|
||||
|
||||
@@ -38,6 +38,10 @@ type pollingMon struct {
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (pm *pollingMon) IsInterestingInterface(iface string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (pm *pollingMon) Close() error {
|
||||
pm.closeOnce.Do(func() {
|
||||
close(pm.stop)
|
||||
|
||||
@@ -651,6 +651,21 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
// SetKeepAlive so that idle connections to peers that have forgotten about
|
||||
// the connection or gone completely offline eventually time out.
|
||||
// Applications might be setting this on a forwarded connection, but from
|
||||
// userspace we can not see those, so the best we can do is to always
|
||||
// perform them with conservative timing.
|
||||
// TODO(tailscale/tailscale#4522): Netstack defaults match the Linux
|
||||
// defaults, and results in a little over two hours before the socket would
|
||||
// be closed due to keepalive. A shorter default might be better, or seeking
|
||||
// a default from the host IP stack. This also might be a useful
|
||||
// user-tunable, as in userspace mode this can have broad implications such
|
||||
// as lingering connections to fork style daemons. On the other side of the
|
||||
// fence, the long duration timers are low impact values for battery powered
|
||||
// peers.
|
||||
ep.SocketOptions().SetKeepAlive(true)
|
||||
|
||||
// The ForwarderRequest.CreateEndpoint above asynchronously
|
||||
// starts the TCP handshake. Note that the gonet.TCPConn
|
||||
// methods c.RemoteAddr() and c.LocalAddr() will return nil
|
||||
@@ -662,12 +677,9 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
c := gonet.NewTCPConn(&wq, ep)
|
||||
|
||||
if ns.lb != nil {
|
||||
if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) && handleSSH != nil {
|
||||
ns.logf("handling SSH connection....")
|
||||
if err := handleSSH(ns.logf, ns.lb, c); err != nil {
|
||||
if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) {
|
||||
if err := ns.lb.HandleSSHConn(c); err != nil {
|
||||
ns.logf("ssh error: %v", err)
|
||||
} else {
|
||||
ns.logf("ssh: ok")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || (darwin && !ios)
|
||||
// +build linux darwin,!ios
|
||||
|
||||
package netstack
|
||||
|
||||
import "tailscale.com/ssh/tailssh"
|
||||
|
||||
func init() {
|
||||
handleSSH = tailssh.Handle
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -115,14 +116,11 @@ func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wra
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.pendOpen == nil {
|
||||
e.pendOpen = make(map[flowtrack.Tuple]*pendingOpenFlow)
|
||||
}
|
||||
if _, dup := e.pendOpen[flow]; dup {
|
||||
// Duplicates are expected when the OS retransmits. Ignore.
|
||||
return
|
||||
}
|
||||
e.pendOpen[flow] = &pendingOpenFlow{timer: timer}
|
||||
mak.Set(&e.pendOpen, flow, &pendingOpenFlow{timer: timer})
|
||||
|
||||
return filter.Accept
|
||||
}
|
||||
|
||||
@@ -1514,8 +1514,14 @@ func supportsV6NAT() bool {
|
||||
// Can't read the file. Assume SNAT works.
|
||||
return true
|
||||
}
|
||||
|
||||
return bytes.Contains(bs, []byte("nat\n"))
|
||||
if bytes.Contains(bs, []byte("nat\n")) {
|
||||
return true
|
||||
}
|
||||
// In nftables mode, that proc file will be empty. Try another thing:
|
||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIPRuleSupportsV6(logf logger.Logf) error {
|
||||
|
||||
@@ -1203,7 +1203,10 @@ func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) {
|
||||
why := "link-change-minor"
|
||||
if changed {
|
||||
why = "link-change-major"
|
||||
metricNumMajorChanges.Add(1)
|
||||
e.magicConn.Rebind()
|
||||
} else {
|
||||
metricNumMinorChanges.Add(1)
|
||||
}
|
||||
e.magicConn.ReSTUN(why)
|
||||
}
|
||||
@@ -1551,4 +1554,7 @@ func (ls fwdDNSLinkSelector) PickLink(ip netaddr.IP) (linkName string) {
|
||||
var (
|
||||
metricMagicDNSPacketIn = clientmetric.NewGauge("magicdns_packet_in") // for 100.100.100.100
|
||||
metricReflectToOS = clientmetric.NewGauge("packet_reflect_to_os")
|
||||
|
||||
metricNumMajorChanges = clientmetric.NewCounter("wgengine_major_changes")
|
||||
metricNumMinorChanges = clientmetric.NewCounter("wgengine_minor_changes")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user