Compare commits
31 Commits
bradfitz/s
...
cross-andr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20cd690970 | ||
|
|
26fe00e7bd | ||
|
|
972df80be2 | ||
|
|
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 |
53
.github/workflows/cross-android.yml
vendored
Normal file
53
.github/workflows/cross-android.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Android-Cross
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Android build cmd
|
||||
env:
|
||||
GOOS: android
|
||||
GOARCH: amd64
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Android build tests (does not run tests)
|
||||
env:
|
||||
GOOS: android
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -run '^$' -c ); done
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
|
||||
4
go.mod
4
go.mod
@@ -40,7 +40,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-20220420224200-c602b5dfaa7f
|
||||
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
|
||||
@@ -49,7 +49,7 @@ 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-20220411220226-7b82a4e95df4
|
||||
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-20220412211240-33da011f77ad
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1067,8 +1067,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-20220420224200-c602b5dfaa7f h1:3CuODoSnBXS+ZkQlGakDqtX1o2RteR1870yF+dS61PY=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f/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 +1227,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-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/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=
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -344,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()
|
||||
@@ -1808,6 +1809,10 @@ func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
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
|
||||
}
|
||||
@@ -2207,7 +2212,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2236,7 +2241,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2267,16 +2272,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")
|
||||
//
|
||||
|
||||
@@ -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 {
|
||||
@@ -1047,3 +1062,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 }
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ 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"
|
||||
@@ -49,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).
|
||||
@@ -228,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
|
||||
}
|
||||
@@ -334,23 +330,6 @@ func (f *forwarder) packetListener(ip netaddr.IP) (packetListener, error) {
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
// getKnownDoHClient returns an HTTP client for a DoH provider (such as Google
|
||||
// or Cloudflare DNS), as a function of one of its (usually four) IPs.
|
||||
//
|
||||
// The provided IP is only used to determine the DoH provider; it is not
|
||||
// prioritized among the set of IPs that are used by the provider.
|
||||
func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Client, ok bool) {
|
||||
urlBase, ok = publicdns.KnownDoH()[ip]
|
||||
if !ok {
|
||||
return "", nil, false
|
||||
}
|
||||
c, ok = f.getKnownDoHClientForProvider(urlBase)
|
||||
if !ok {
|
||||
return "", nil, false
|
||||
}
|
||||
return urlBase, c, true
|
||||
}
|
||||
|
||||
// getKnownDoHClientForProvider returns an HTTP client for a specific DoH
|
||||
// provider named by its DoH base URL (like "https://dns.google/dns-query").
|
||||
//
|
||||
@@ -444,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)
|
||||
@@ -615,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
|
||||
@@ -655,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 {
|
||||
@@ -674,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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ package resolver
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -25,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
|
||||
@@ -46,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,
|
||||
})
|
||||
}
|
||||
@@ -65,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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -725,6 +725,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 +749,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 {
|
||||
|
||||
@@ -382,6 +382,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 +756,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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -59,11 +59,11 @@ 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 == "" {
|
||||
if ss.conn.srv.tailscaledPath == "" {
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
lu := ss.localUser
|
||||
ci := ss.connInfo
|
||||
lu := ss.conn.localUser
|
||||
ci := ss.conn.info
|
||||
remoteUser := ci.uprof.LoginName
|
||||
if len(ci.node.Tags) > 0 {
|
||||
remoteUser = strings.Join(ci.node.Tags, ",")
|
||||
@@ -85,7 +85,7 @@ func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args
|
||||
incubatorArgs = append(incubatorArgs, args...)
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, ss.srv.tailscaledPath, incubatorArgs...)
|
||||
return exec.CommandContext(ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
|
||||
}
|
||||
|
||||
const debugIncubator = false
|
||||
@@ -166,7 +166,7 @@ func beIncubator(args []string) error {
|
||||
//
|
||||
// It sets ss.cmd, stdin, stdout, and stderr.
|
||||
func (ss *sshSession) launchProcess(ctx context.Context) error {
|
||||
shell := loginShell(ss.localUser.Uid)
|
||||
shell := loginShell(ss.conn.localUser.Uid)
|
||||
var args []string
|
||||
if rawCmd := ss.RawCommand(); rawCmd != "" {
|
||||
args = append(args, "-c", rawCmd)
|
||||
@@ -174,10 +174,10 @@ func (ss *sshSession) launchProcess(ctx context.Context) error {
|
||||
args = append(args, "-l") // login shell
|
||||
}
|
||||
|
||||
ci := ss.connInfo
|
||||
ci := ss.conn.info
|
||||
cmd := ss.newIncubatorCommand(ctx, shell, args)
|
||||
cmd.Dir = ss.localUser.HomeDir
|
||||
cmd.Env = append(cmd.Env, envForUser(ss.localUser)...)
|
||||
cmd.Dir = ss.conn.localUser.HomeDir
|
||||
cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
|
||||
cmd.Env = append(cmd.Env, ss.Environ()...)
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.IP(), ci.src.Port(), ci.dst.Port()),
|
||||
|
||||
@@ -43,8 +43,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
debugPolicyFile = envknob.String("TS_DEBUG_SSH_POLICY_FILE")
|
||||
debugIgnoreTailnetSSHPolicy = envknob.Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY")
|
||||
debugPolicyFile = envknob.SSHPolicyFile()
|
||||
debugIgnoreTailnetSSHPolicy = envknob.SSHIgnoreTailnetPolicy()
|
||||
sshVerboseLogging = envknob.Bool("TS_DEBUG_SSH_VLOG")
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ func init() {
|
||||
|
||||
// HandleSSHConn handles a Tailscale SSH connection from c.
|
||||
func (srv *server) HandleSSHConn(c net.Conn) error {
|
||||
ss, err := srv.newSSHServer()
|
||||
ss, err := srv.newConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,34 +109,135 @@ func (srv *server) OnPolicyChange() {
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *server) newSSHServer() (*ssh.Server, error) {
|
||||
ss := &ssh.Server{
|
||||
Handler: srv.handleSSH,
|
||||
// conn represents a single SSH connection and its associated
|
||||
// ssh.Server.
|
||||
type conn struct {
|
||||
*ssh.Server
|
||||
|
||||
// now is the time to consider the present moment for the
|
||||
// purposes of rule evaluation.
|
||||
now time.Time
|
||||
|
||||
action0 *tailcfg.SSHAction // first matching action
|
||||
srv *server
|
||||
info *sshConnInfo // set by setInfo
|
||||
localUser *user.User // set by checkAuth
|
||||
|
||||
insecureSkipTailscaleAuth bool // used by tests.
|
||||
}
|
||||
|
||||
func (c *conn) logf(format string, args ...any) {
|
||||
if c.info == nil {
|
||||
c.srv.logf(format, args...)
|
||||
return
|
||||
}
|
||||
format = fmt.Sprintf("%v: %v", c.info.String(), format)
|
||||
c.srv.logf(format, args...)
|
||||
}
|
||||
|
||||
// PublicKeyHandler implements ssh.PublicKeyHandler is called by the the
|
||||
// ssh.Server when the client presents a public key.
|
||||
func (c *conn) PublicKeyHandler(ctx ssh.Context, pubKey ssh.PublicKey) error {
|
||||
if c.info == nil {
|
||||
return gossh.ErrDenied
|
||||
}
|
||||
if err := c.checkAuth(pubKey); err != nil {
|
||||
// TODO(maisem/bradfitz): surface the error here.
|
||||
c.logf("rejecting SSH public key %s: %v", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)), err)
|
||||
return err
|
||||
}
|
||||
c.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// errPubKeyRequired is returned by NoClientAuthCallback to make the client
|
||||
// resort to public-key auth; not user visible.
|
||||
var errPubKeyRequired = errors.New("ssh publickey required")
|
||||
|
||||
// NoClientAuthCallback implements gossh.NoClientAuthCallback and is called by
|
||||
// the the ssh.Server when the client first connects with the "none"
|
||||
// authentication method.
|
||||
func (c *conn) NoClientAuthCallback(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
if c.insecureSkipTailscaleAuth {
|
||||
return nil, nil
|
||||
}
|
||||
if err := c.setInfo(cm); err != nil {
|
||||
c.logf("failed to get conninfo: %v", err)
|
||||
return nil, gossh.ErrDenied
|
||||
}
|
||||
return nil, c.checkAuth(nil /* no pub key */)
|
||||
}
|
||||
|
||||
// checkAuth verifies that conn can proceed with the specified (optional)
|
||||
// pubKey. It returns nil if the matching policy action is Accept or
|
||||
// HoldAndDelegate. If pubKey is nil, there was no policy match but there is a
|
||||
// policy that might match a public key it returns errPubKeyRequired. Otherwise,
|
||||
// it returns gossh.ErrDenied possibly wrapped in gossh.WithBannerError.
|
||||
func (c *conn) checkAuth(pubKey ssh.PublicKey) error {
|
||||
a, localUser, err := c.evaluatePolicy(pubKey)
|
||||
if err != nil {
|
||||
if pubKey == nil && c.havePubKeyPolicy(c.info) {
|
||||
return errPubKeyRequired
|
||||
}
|
||||
return fmt.Errorf("%w: %v", gossh.ErrDenied, err)
|
||||
}
|
||||
c.action0 = a
|
||||
if a.Accept || a.HoldAndDelegate != "" {
|
||||
lu, err := user.Lookup(localUser)
|
||||
if err != nil {
|
||||
c.logf("failed to lookup %v: %v", localUser, err)
|
||||
return gossh.WithBannerError{
|
||||
Err: gossh.ErrDenied,
|
||||
Message: fmt.Sprintf("failed to lookup %v\r\n", localUser),
|
||||
}
|
||||
}
|
||||
c.localUser = lu
|
||||
return nil
|
||||
}
|
||||
if a.Reject {
|
||||
err := gossh.ErrDenied
|
||||
if a.Message != "" {
|
||||
err = gossh.WithBannerError{
|
||||
Err: err,
|
||||
Message: a.Message,
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Shouldn't get here, but:
|
||||
return gossh.ErrDenied
|
||||
}
|
||||
|
||||
// ServerConfig implements ssh.ServerConfigCallback.
|
||||
func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
return &gossh.ServerConfig{
|
||||
// OpenSSH presents this on failure as `Permission denied (tailscale).`
|
||||
ImplictAuthMethod: "tailscale",
|
||||
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
||||
NoClientAuthCallback: c.NoClientAuthCallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *server) newConn() (*conn, error) {
|
||||
c := &conn{srv: srv, now: srv.now()}
|
||||
c.Server = &ssh.Server{
|
||||
Version: "Tailscale",
|
||||
Handler: c.handleConnPostSSHAuth,
|
||||
RequestHandlers: map[string]ssh.RequestHandler{},
|
||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{},
|
||||
|
||||
// Note: the direct-tcpip channel handler and LocalPortForwardingCallback
|
||||
// only adds support for forwarding ports from the local machine.
|
||||
// TODO(maisem/bradfitz): add remote port forwarding support.
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
||||
},
|
||||
Version: "SSH-2.0-Tailscale",
|
||||
LocalPortForwardingCallback: srv.mayForwardLocalPortTo,
|
||||
NoClientAuthCallback: func(m gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
if srv.requiresPubKey(m.User(), toIPPort(m.LocalAddr()), toIPPort(m.RemoteAddr())) {
|
||||
return nil, errors.New("public key required") // any non-nil error will do
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
if srv.acceptPubKey(ctx.User(), toIPPort(ctx.LocalAddr()), toIPPort(ctx.RemoteAddr()), key) {
|
||||
srv.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(key)))
|
||||
return true
|
||||
}
|
||||
srv.logf("rejecting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(key)))
|
||||
return false
|
||||
},
|
||||
|
||||
PublicKeyHandler: c.PublicKeyHandler,
|
||||
ServerConfigCallback: c.ServerConfig,
|
||||
}
|
||||
ss := c.Server
|
||||
for k, v := range ssh.DefaultRequestHandlers {
|
||||
ss.RequestHandlers[k] = v
|
||||
}
|
||||
@@ -153,7 +254,7 @@ func (srv *server) newSSHServer() (*ssh.Server, error) {
|
||||
for _, signer := range keys {
|
||||
ss.AddHostKey(signer)
|
||||
}
|
||||
return ss, nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
|
||||
@@ -167,37 +268,24 @@ func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string
|
||||
return ss.action.AllowLocalPortForwarding
|
||||
}
|
||||
|
||||
// requiresPubKey reports whether the SSH server, during the auth negotiation
|
||||
// phase, should requires that the client send an SSH public key. (or, more
|
||||
// specifically, that "none" auth isn't acceptable)
|
||||
func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr netaddr.IPPort) bool {
|
||||
pol, ok := srv.sshPolicy()
|
||||
// havePubKeyPolicy reports whether any policy rule may provide access by means
|
||||
// of a ssh.PublicKey.
|
||||
func (c *conn) havePubKeyPolicy(ci *sshConnInfo) bool {
|
||||
// Is there any rule that looks like it'd require a public key for this
|
||||
// sshUser?
|
||||
pol, ok := c.sshPolicy()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
a, ci, _, err := srv.evaluatePolicy(sshUser, localAddr, remoteAddr, nil)
|
||||
if err == nil && (a.Accept || a.HoldAndDelegate != "") {
|
||||
// Policy doesn't require a public key.
|
||||
return false
|
||||
}
|
||||
if ci == nil {
|
||||
// If we didn't get far enough along through evaluatePolicy to know the Tailscale
|
||||
// identify of the remote side then it's going to fail quickly later anyway.
|
||||
// Return false to accept "none" auth and reject the conn.
|
||||
return false
|
||||
}
|
||||
|
||||
// Is there any rule that looks like it'd require a public key for this
|
||||
// sshUser?
|
||||
for _, r := range pol.Rules {
|
||||
if ci.ruleExpired(r) {
|
||||
if c.ruleExpired(r) {
|
||||
continue
|
||||
}
|
||||
if mapLocalUser(r.SSHUsers, sshUser) == "" {
|
||||
if mapLocalUser(r.SSHUsers, ci.sshUser) == "" {
|
||||
continue
|
||||
}
|
||||
for _, p := range r.Principals {
|
||||
if principalMatchesTailscaleIdentity(p, ci) && len(p.PubKeys) > 0 {
|
||||
if len(p.PubKeys) > 0 && c.principalMatchesTailscaleIdentity(p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -205,19 +293,11 @@ func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr netaddr.
|
||||
return false
|
||||
}
|
||||
|
||||
func (srv *server) acceptPubKey(sshUser string, localAddr, remoteAddr netaddr.IPPort, pubKey ssh.PublicKey) bool {
|
||||
a, _, _, err := srv.evaluatePolicy(sshUser, localAddr, remoteAddr, pubKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return a.Accept || a.HoldAndDelegate != ""
|
||||
}
|
||||
|
||||
// sshPolicy returns the SSHPolicy for current node.
|
||||
// If there is no SSHPolicy in the netmap, it returns a debugPolicy
|
||||
// if one is defined.
|
||||
func (srv *server) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
|
||||
lb := srv.lb
|
||||
func (c *conn) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
|
||||
lb := c.srv.lb
|
||||
nm := lb.NetMap()
|
||||
if nm == nil {
|
||||
return nil, false
|
||||
@@ -226,14 +306,15 @@ func (srv *server) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
|
||||
return pol, true
|
||||
}
|
||||
if debugPolicyFile != "" {
|
||||
c.logf("reading debug SSH policy file: %v", debugPolicyFile)
|
||||
f, err := os.ReadFile(debugPolicyFile)
|
||||
if err != nil {
|
||||
srv.logf("error reading debug SSH policy file: %v", err)
|
||||
c.logf("error reading debug SSH policy file: %v", err)
|
||||
return nil, false
|
||||
}
|
||||
p := new(tailcfg.SSHPolicy)
|
||||
if err := json.Unmarshal(f, p); err != nil {
|
||||
srv.logf("invalid JSON in %v: %v", debugPolicyFile, err)
|
||||
c.logf("invalid JSON in %v: %v", debugPolicyFile, err)
|
||||
return nil, false
|
||||
}
|
||||
return p, true
|
||||
@@ -253,42 +334,45 @@ func toIPPort(a net.Addr) (ipp netaddr.IPPort) {
|
||||
return netaddr.IPPortFrom(tanetaddr, uint16(ta.Port))
|
||||
}
|
||||
|
||||
// connInfo returns a populated sshConnInfo from the provided arguments,
|
||||
// validating only that they represent a known Tailscale identity.
|
||||
func (c *conn) setInfo(cm gossh.ConnMetadata) error {
|
||||
ci := &sshConnInfo{
|
||||
sshUser: cm.User(),
|
||||
src: toIPPort(cm.RemoteAddr()),
|
||||
dst: toIPPort(cm.LocalAddr()),
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(ci.dst.IP()) {
|
||||
return fmt.Errorf("tailssh: rejecting non-Tailscale local address %v", ci.dst)
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(ci.src.IP()) {
|
||||
return fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", ci.src)
|
||||
}
|
||||
node, uprof, ok := c.srv.lb.WhoIs(ci.src)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown Tailscale identity from src %v", ci.src)
|
||||
}
|
||||
ci.node = node
|
||||
ci.uprof = &uprof
|
||||
|
||||
c.info = ci
|
||||
return nil
|
||||
}
|
||||
|
||||
// evaluatePolicy returns the SSHAction, sshConnInfo and localUser after
|
||||
// evaluating the sshUser and remoteAddr against the SSHPolicy. The remoteAddr
|
||||
// and localAddr params must be Tailscale IPs.
|
||||
//
|
||||
// The return sshConnInfo will be non-nil, even on some errors, if the
|
||||
// evaluation made it far enough to resolve the remoteAddr to a Tailscale IP.
|
||||
func (srv *server) evaluatePolicy(sshUser string, localAddr, remoteAddr netaddr.IPPort, pubKey ssh.PublicKey) (_ *tailcfg.SSHAction, _ *sshConnInfo, localUser string, _ error) {
|
||||
pol, ok := srv.sshPolicy()
|
||||
// and localAddr params must be Tailscale IPs. The pubKey may be nil for "none"
|
||||
// auth.
|
||||
func (c *conn) evaluatePolicy(pubKey gossh.PublicKey) (_ *tailcfg.SSHAction, localUser string, _ error) {
|
||||
pol, ok := c.sshPolicy()
|
||||
if !ok {
|
||||
return nil, nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
|
||||
return nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(remoteAddr.IP()) {
|
||||
return nil, nil, "", fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", remoteAddr)
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(localAddr.IP()) {
|
||||
return nil, nil, "", fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", localAddr)
|
||||
}
|
||||
node, uprof, ok := srv.lb.WhoIs(remoteAddr)
|
||||
a, localUser, ok := c.evalSSHPolicy(pol, pubKey)
|
||||
if !ok {
|
||||
return nil, nil, "", fmt.Errorf("unknown Tailscale identity from src %v", remoteAddr)
|
||||
return nil, "", fmt.Errorf("tailssh: rejecting connection; no matching policy")
|
||||
}
|
||||
ci := &sshConnInfo{
|
||||
now: srv.now(),
|
||||
fetchPublicKeysURL: srv.fetchPublicKeysURL,
|
||||
sshUser: sshUser,
|
||||
src: remoteAddr,
|
||||
dst: localAddr,
|
||||
node: node,
|
||||
uprof: &uprof,
|
||||
pubKey: pubKey,
|
||||
}
|
||||
a, localUser, ok := evalSSHPolicy(pol, ci)
|
||||
if !ok {
|
||||
return nil, ci, "", fmt.Errorf("ssh: access denied for %q from %v", uprof.LoginName, ci.src.IP())
|
||||
}
|
||||
return a, ci, localUser, nil
|
||||
return a, localUser, nil
|
||||
}
|
||||
|
||||
// pubKeyCacheEntry is the cache value for an HTTPS URL of public keys (like
|
||||
@@ -334,6 +418,9 @@ func (srv *server) pubKeyClient() *http.Client {
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
// fetchPublicKeysURL fetches the public keys from a URL. The strings are in the
|
||||
// the typical public key "type base64-string [comment]" format seen at e.g.
|
||||
// https://github.com/USER.keys
|
||||
func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
return nil, errors.New("invalid URL scheme")
|
||||
@@ -386,52 +473,38 @@ func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
|
||||
return lines, err
|
||||
}
|
||||
|
||||
// handleSSH is invoked when a new SSH connection attempt is made.
|
||||
func (srv *server) handleSSH(s ssh.Session) {
|
||||
logf := srv.logf
|
||||
|
||||
// handleConnPostSSHAuth runs an SSH session after the SSH-level authentication,
|
||||
// but not necessarily before all the Tailscale-level extra verification has
|
||||
// completed.
|
||||
func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
|
||||
sshUser := s.User()
|
||||
action, ci, localUser, err := srv.evaluatePolicy(sshUser, toIPPort(s.LocalAddr()), toIPPort(s.RemoteAddr()), s.PublicKey())
|
||||
action, err := c.resolveTerminalAction(s)
|
||||
if err != nil {
|
||||
logf(err.Error())
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
var lu *user.User
|
||||
if localUser != "" {
|
||||
lu, err = user.Lookup(localUser)
|
||||
if err != nil {
|
||||
logf("ssh: user Lookup %q: %v", localUser, err)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
ss := srv.newSSHSession(s, ci, lu)
|
||||
ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", ci.uprof.LoginName, ci.src.IP(), sshUser)
|
||||
action, err = ss.resolveTerminalAction(action)
|
||||
if err != nil {
|
||||
ss.logf("resolveTerminalAction: %v", err)
|
||||
io.WriteString(s.Stderr(), "Access denied: failed to resolve SSHAction.\n")
|
||||
c.logf("resolveTerminalAction: %v", err)
|
||||
io.WriteString(s.Stderr(), "Access Denied: failed during authorization check.\r\n")
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if action.Reject || !action.Accept {
|
||||
ss.logf("access denied for %v (%v)", ci.uprof.LoginName, ci.src.IP())
|
||||
c.logf("access denied for %v", c.info.uprof.LoginName)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
ss.logf("access granted for %v (%v) to ssh-user %q", ci.uprof.LoginName, ci.src.IP(), sshUser)
|
||||
ss.action = action
|
||||
|
||||
ss := c.newSSHSession(s, action)
|
||||
ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", c.info.uprof.LoginName, c.info.src.IP(), sshUser)
|
||||
ss.logf("access granted to %v as ssh-user %q", c.info.uprof.LoginName, sshUser)
|
||||
ss.run()
|
||||
}
|
||||
|
||||
// resolveTerminalAction either returns action (if it's Accept or Reject) or else
|
||||
// loops, fetching new SSHActions from the control plane.
|
||||
// resolveTerminalAction either returns action0 (if it's Accept or Reject) or
|
||||
// else loops, fetching new SSHActions from the control plane.
|
||||
//
|
||||
// Any action with a Message in the chain will be printed to ss.
|
||||
// Any action with a Message in the chain will be printed to s.
|
||||
//
|
||||
// The returned SSHAction will be either Reject or Accept.
|
||||
func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg.SSHAction, error) {
|
||||
func (c *conn) resolveTerminalAction(s ssh.Session) (*tailcfg.SSHAction, error) {
|
||||
action := c.action0
|
||||
// Loop processing/fetching Actions until one reaches a
|
||||
// terminal state (Accept, Reject, or invalid Action), or
|
||||
// until fetchSSHAction times out due to the context being
|
||||
@@ -440,7 +513,7 @@ func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg
|
||||
// instructions and go to a URL to do something.)
|
||||
for {
|
||||
if action.Message != "" {
|
||||
io.WriteString(ss.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
|
||||
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
|
||||
}
|
||||
if action.Accept || action.Reject {
|
||||
return action, nil
|
||||
@@ -449,31 +522,48 @@ func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg
|
||||
if url == "" {
|
||||
return nil, errors.New("reached Action that lacked Accept, Reject, and HoldAndDelegate")
|
||||
}
|
||||
url = ss.expandDelegateURL(url)
|
||||
url = c.expandDelegateURL(url)
|
||||
var err error
|
||||
action, err = ss.srv.fetchSSHAction(ss.Context(), url)
|
||||
action, err = c.fetchSSHAction(s.Context(), url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching SSHAction from %s: %w", url, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *sshSession) expandDelegateURL(actionURL string) string {
|
||||
nm := ss.srv.lb.NetMap()
|
||||
func (c *conn) expandDelegateURL(actionURL string) string {
|
||||
nm := c.srv.lb.NetMap()
|
||||
ci := c.info
|
||||
var dstNodeID string
|
||||
if nm != nil {
|
||||
dstNodeID = fmt.Sprint(int64(nm.SelfNode.ID))
|
||||
}
|
||||
return strings.NewReplacer(
|
||||
"$SRC_NODE_IP", url.QueryEscape(ss.connInfo.src.IP().String()),
|
||||
"$SRC_NODE_ID", fmt.Sprint(int64(ss.connInfo.node.ID)),
|
||||
"$DST_NODE_IP", url.QueryEscape(ss.connInfo.dst.IP().String()),
|
||||
"$SRC_NODE_IP", url.QueryEscape(ci.src.IP().String()),
|
||||
"$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID)),
|
||||
"$DST_NODE_IP", url.QueryEscape(ci.dst.IP().String()),
|
||||
"$DST_NODE_ID", dstNodeID,
|
||||
"$SSH_USER", url.QueryEscape(ss.connInfo.sshUser),
|
||||
"$LOCAL_USER", url.QueryEscape(ss.localUser.Username),
|
||||
"$SSH_USER", url.QueryEscape(ci.sshUser),
|
||||
"$LOCAL_USER", url.QueryEscape(c.localUser.Username),
|
||||
).Replace(actionURL)
|
||||
}
|
||||
|
||||
func (c *conn) expandPublicKeyURL(pubKeyURL string) string {
|
||||
if !strings.Contains(pubKeyURL, "$") {
|
||||
return pubKeyURL
|
||||
}
|
||||
var localPart string
|
||||
var loginName string
|
||||
if c.info.uprof != nil {
|
||||
loginName = c.info.uprof.LoginName
|
||||
localPart, _, _ = strings.Cut(loginName, "@")
|
||||
}
|
||||
return strings.NewReplacer(
|
||||
"$LOGINNAME_EMAIL", loginName,
|
||||
"$LOGINNAME_LOCALPART", localPart,
|
||||
).Replace(pubKeyURL)
|
||||
}
|
||||
|
||||
// sshSession is an accepted Tailscale SSH session.
|
||||
type sshSession struct {
|
||||
ssh.Session
|
||||
@@ -482,10 +572,8 @@ type sshSession struct {
|
||||
logf logger.Logf
|
||||
|
||||
ctx *sshContext // implements context.Context
|
||||
srv *server
|
||||
connInfo *sshConnInfo
|
||||
conn *conn
|
||||
action *tailcfg.SSHAction
|
||||
localUser *user.User
|
||||
agentListener net.Listener // non-nil if agent-forwarding requested+allowed
|
||||
|
||||
// initialized by launchProcess:
|
||||
@@ -506,39 +594,48 @@ func (ss *sshSession) vlogf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *server) newSSHSession(s ssh.Session, ci *sshConnInfo, lu *user.User) *sshSession {
|
||||
sharedID := fmt.Sprintf("%s-%02x", ci.now.UTC().Format("20060102T150405"), randBytes(5))
|
||||
func (c *conn) newSSHSession(s ssh.Session, action *tailcfg.SSHAction) *sshSession {
|
||||
sharedID := fmt.Sprintf("%s-%02x", c.now.UTC().Format("20060102T150405"), randBytes(5))
|
||||
c.logf("starting session: %v", sharedID)
|
||||
return &sshSession{
|
||||
Session: s,
|
||||
idH: s.Context().(ssh.Context).SessionID(),
|
||||
sharedID: sharedID,
|
||||
ctx: newSSHContext(),
|
||||
srv: srv,
|
||||
localUser: lu,
|
||||
connInfo: ci,
|
||||
logf: logger.WithPrefix(srv.logf, "ssh-session("+sharedID+"): "),
|
||||
Session: s,
|
||||
idH: s.Context().(ssh.Context).SessionID(),
|
||||
sharedID: sharedID,
|
||||
ctx: newSSHContext(),
|
||||
conn: c,
|
||||
logf: logger.WithPrefix(c.srv.logf, "ssh-session("+sharedID+"): "),
|
||||
action: action,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) isStillValid(pubKey ssh.PublicKey) bool {
|
||||
a, localUser, err := c.evaluatePolicy(pubKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !a.Accept && a.HoldAndDelegate == "" {
|
||||
return false
|
||||
}
|
||||
return c.localUser.Username == localUser
|
||||
}
|
||||
|
||||
// checkStillValid checks that the session is still valid per the latest SSHPolicy.
|
||||
// If not, it terminates the session.
|
||||
func (ss *sshSession) checkStillValid() {
|
||||
ci := ss.connInfo
|
||||
a, _, lu, err := ss.srv.evaluatePolicy(ci.sshUser, ci.src, ci.dst, ci.pubKey)
|
||||
if err == nil && (a.Accept || a.HoldAndDelegate != "") && lu == ss.localUser.Username {
|
||||
if ss.conn.isStillValid(ss.PublicKey()) {
|
||||
return
|
||||
}
|
||||
ss.logf("session no longer valid per new SSH policy; closing")
|
||||
ss.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Access revoked.\n"),
|
||||
fmt.Sprintf("Access revoked.\r\n"),
|
||||
context.Canceled,
|
||||
})
|
||||
}
|
||||
|
||||
func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
|
||||
func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
|
||||
defer cancel()
|
||||
bo := backoff.NewBackoff("fetch-ssh-action", srv.logf, 10*time.Second)
|
||||
bo := backoff.NewBackoff("fetch-ssh-action", c.logf, 10*time.Second)
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
@@ -547,7 +644,7 @@ func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSH
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := srv.lb.DoNoiseRequest(req)
|
||||
res, err := c.srv.lb.DoNoiseRequest(req)
|
||||
if err != nil {
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
@@ -558,7 +655,7 @@ func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSH
|
||||
if len(body) > 1<<10 {
|
||||
body = body[:1<<10]
|
||||
}
|
||||
srv.logf("fetch of %v: %s, %s", url, res.Status, body)
|
||||
c.logf("fetch of %v: %s, %s", url, res.Status, body)
|
||||
bo.BackOff(ctx, fmt.Errorf("unexpected status: %v", res.Status))
|
||||
continue
|
||||
}
|
||||
@@ -566,7 +663,7 @@ func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSH
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
srv.logf("invalid next SSHAction JSON from %v: %v", url, err)
|
||||
c.logf("invalid next SSHAction JSON from %v: %v", url, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
@@ -588,7 +685,7 @@ func (ss *sshSession) killProcessOnContextDone() {
|
||||
io.WriteString(ss.Stderr(), "\r\n\r\n"+msg+"\r\n\r\n")
|
||||
}
|
||||
}
|
||||
ss.logf("terminating SSH session from %v: %v", ss.connInfo.src.IP(), err)
|
||||
ss.logf("terminating SSH session from %v: %v", ss.conn.info.src.IP(), err)
|
||||
ss.cmd.Process.Kill()
|
||||
})
|
||||
}
|
||||
@@ -683,7 +780,7 @@ var recordSSH = envknob.Bool("TS_DEBUG_LOG_SSH")
|
||||
// It handles ss once it's been accepted and determined
|
||||
// that it should run.
|
||||
func (ss *sshSession) run() {
|
||||
srv := ss.srv
|
||||
srv := ss.conn.srv
|
||||
srv.startSession(ss)
|
||||
defer srv.endSession(ss)
|
||||
|
||||
@@ -700,13 +797,13 @@ func (ss *sshSession) run() {
|
||||
}
|
||||
|
||||
logf := srv.logf
|
||||
lu := ss.localUser
|
||||
lu := ss.conn.localUser
|
||||
localUser := lu.Username
|
||||
|
||||
if euid := os.Geteuid(); euid != 0 {
|
||||
if lu.Uid != fmt.Sprint(euid) {
|
||||
ss.logf("can't switch to user %q from process euid %v", localUser, euid)
|
||||
fmt.Fprintf(ss, "can't switch user\n")
|
||||
fmt.Fprintf(ss, "can't switch user\r\n")
|
||||
ss.Exit(1)
|
||||
return
|
||||
}
|
||||
@@ -728,7 +825,7 @@ func (ss *sshSession) run() {
|
||||
var err error
|
||||
rec, err = ss.startNewRecording()
|
||||
if err != nil {
|
||||
fmt.Fprintf(ss, "can't start new recording\n")
|
||||
fmt.Fprintf(ss, "can't start new recording\r\n")
|
||||
ss.logf("startNewRecording: %v", err)
|
||||
ss.Exit(1)
|
||||
return
|
||||
@@ -801,14 +898,6 @@ func (ss *sshSession) shouldRecord() bool {
|
||||
}
|
||||
|
||||
type sshConnInfo struct {
|
||||
// now is the time to consider the present moment for the
|
||||
// purposes of rule evaluation.
|
||||
now time.Time
|
||||
// fetchPublicKeysURL, if non-nil, is a func to fetch the public
|
||||
// keys of a URL. The strings are in the the typical public
|
||||
// key "type base64-string [comment]" format seen at e.g. https://github.com/USER.keys
|
||||
fetchPublicKeysURL func(url string) ([]string, error)
|
||||
|
||||
// sshUser is the requested local SSH username ("root", "alice", etc).
|
||||
sshUser string
|
||||
|
||||
@@ -823,23 +912,22 @@ type sshConnInfo struct {
|
||||
|
||||
// uprof is node's UserProfile.
|
||||
uprof *tailcfg.UserProfile
|
||||
|
||||
// pubKey is the public key presented by the client, or nil
|
||||
// if they haven't yet sent one (as in the early "none" phase
|
||||
// of authentication negotiation).
|
||||
pubKey ssh.PublicKey
|
||||
}
|
||||
|
||||
func (ci *sshConnInfo) ruleExpired(r *tailcfg.SSHRule) bool {
|
||||
func (ci *sshConnInfo) String() string {
|
||||
return fmt.Sprintf("%v->%v@%v", ci.src, ci.sshUser, ci.dst)
|
||||
}
|
||||
|
||||
func (c *conn) ruleExpired(r *tailcfg.SSHRule) bool {
|
||||
if r.RuleExpires == nil {
|
||||
return false
|
||||
}
|
||||
return r.RuleExpires.Before(ci.now)
|
||||
return r.RuleExpires.Before(c.now)
|
||||
}
|
||||
|
||||
func evalSSHPolicy(pol *tailcfg.SSHPolicy, ci *sshConnInfo) (a *tailcfg.SSHAction, localUser string, ok bool) {
|
||||
func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, ok bool) {
|
||||
for _, r := range pol.Rules {
|
||||
if a, localUser, err := matchRule(r, ci); err == nil {
|
||||
if a, localUser, err := c.matchRule(r, pubKey); err == nil {
|
||||
return a, localUser, true
|
||||
}
|
||||
}
|
||||
@@ -855,23 +943,25 @@ var (
|
||||
errUserMatch = errors.New("user didn't match")
|
||||
)
|
||||
|
||||
func matchRule(r *tailcfg.SSHRule, ci *sshConnInfo) (a *tailcfg.SSHAction, localUser string, err error) {
|
||||
func (c *conn) matchRule(r *tailcfg.SSHRule, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, err error) {
|
||||
if r == nil {
|
||||
return nil, "", errNilRule
|
||||
}
|
||||
if r.Action == nil {
|
||||
return nil, "", errNilAction
|
||||
}
|
||||
if ci.ruleExpired(r) {
|
||||
if c.ruleExpired(r) {
|
||||
return nil, "", errRuleExpired
|
||||
}
|
||||
if !r.Action.Reject || r.SSHUsers != nil {
|
||||
localUser = mapLocalUser(r.SSHUsers, ci.sshUser)
|
||||
localUser = mapLocalUser(r.SSHUsers, c.info.sshUser)
|
||||
if localUser == "" {
|
||||
return nil, "", errUserMatch
|
||||
}
|
||||
}
|
||||
if !anyPrincipalMatches(r.Principals, ci) {
|
||||
if ok, err := c.anyPrincipalMatches(r.Principals, pubKey); err != nil {
|
||||
return nil, "", err
|
||||
} else if !ok {
|
||||
return nil, "", errPrincipalMatch
|
||||
}
|
||||
return r.Action, localUser, nil
|
||||
@@ -888,27 +978,32 @@ func mapLocalUser(ruleSSHUsers map[string]string, reqSSHUser string) (localUser
|
||||
return v
|
||||
}
|
||||
|
||||
func anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
|
||||
func (c *conn) anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
|
||||
for _, p := range ps {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if principalMatches(p, ci) {
|
||||
return true
|
||||
if ok, err := c.principalMatches(p, pubKey); err != nil {
|
||||
return false, err
|
||||
} else if ok {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func principalMatches(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
|
||||
return principalMatchesTailscaleIdentity(p, ci) &&
|
||||
principalMatchesPubKey(p, ci)
|
||||
func (c *conn) principalMatches(p *tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
|
||||
if !c.principalMatchesTailscaleIdentity(p) {
|
||||
return false, nil
|
||||
}
|
||||
return c.principalMatchesPubKey(p, pubKey)
|
||||
}
|
||||
|
||||
// principalMatchesTailscaleIdentity reports whether one of p's four fields
|
||||
// that match the Tailscale identity match (Node, NodeIP, UserLogin, Any).
|
||||
// This function does not consider PubKeys.
|
||||
func principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
|
||||
func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
|
||||
ci := c.info
|
||||
if p.Any {
|
||||
return true
|
||||
}
|
||||
@@ -926,32 +1021,27 @@ func principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal, ci *sshConnInfo)
|
||||
return false
|
||||
}
|
||||
|
||||
func principalMatchesPubKey(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
|
||||
func (c *conn) principalMatchesPubKey(p *tailcfg.SSHPrincipal, clientPubKey gossh.PublicKey) (bool, error) {
|
||||
if len(p.PubKeys) == 0 {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
if ci.pubKey == nil {
|
||||
return false
|
||||
if clientPubKey == nil {
|
||||
return false, nil
|
||||
}
|
||||
pubKeys := p.PubKeys
|
||||
if len(pubKeys) == 1 && strings.HasPrefix(pubKeys[0], "https://") {
|
||||
if ci.fetchPublicKeysURL == nil {
|
||||
// TODO: log?
|
||||
return false
|
||||
}
|
||||
knownKeys := p.PubKeys
|
||||
if len(knownKeys) == 1 && strings.HasPrefix(knownKeys[0], "https://") {
|
||||
var err error
|
||||
pubKeys, err = ci.fetchPublicKeysURL(pubKeys[0])
|
||||
knownKeys, err = c.srv.fetchPublicKeysURL(c.expandPublicKeyURL(knownKeys[0]))
|
||||
if err != nil {
|
||||
// TODO: log?
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
for _, pubKey := range pubKeys {
|
||||
if pubKeyMatchesAuthorizedKey(ci.pubKey, pubKey) {
|
||||
return true
|
||||
for _, knownKey := range knownKeys {
|
||||
if pubKeyMatchesAuthorizedKey(clientPubKey, knownKey) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func pubKeyMatchesAuthorizedKey(pubKey ssh.PublicKey, wantKey string) bool {
|
||||
@@ -995,7 +1085,7 @@ func (ss *sshSession) startNewRecording() (*recording, error) {
|
||||
ss: ss,
|
||||
start: now,
|
||||
}
|
||||
varRoot := ss.srv.lb.TailscaleVarRoot()
|
||||
varRoot := ss.conn.srv.lb.TailscaleVarRoot()
|
||||
if varRoot == "" {
|
||||
return nil, errors.New("no var root for recording storage")
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestMatchRule(t *testing.T) {
|
||||
Action: someAction,
|
||||
RuleExpires: timePtr(time.Unix(100, 0)),
|
||||
},
|
||||
ci: &sshConnInfo{now: time.Unix(200, 0)},
|
||||
ci: &sshConnInfo{},
|
||||
wantErr: errRuleExpired,
|
||||
},
|
||||
{
|
||||
@@ -178,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)
|
||||
}
|
||||
@@ -215,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"),
|
||||
@@ -233,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")
|
||||
@@ -255,12 +259,13 @@ func TestSSH(t *testing.T) {
|
||||
}
|
||||
return
|
||||
}
|
||||
go ss.HandleConn(c)
|
||||
go sc.HandleConn(c)
|
||||
}
|
||||
}()
|
||||
|
||||
execSSH := func(args ...string) *exec.Cmd {
|
||||
cmd := exec.Command("ssh",
|
||||
"-v",
|
||||
"-p", fmt.Sprint(port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"user@127.0.0.1")
|
||||
@@ -276,7 +281,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 {
|
||||
@@ -403,3 +408,26 @@ func TestPublicKeyFetching(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1636,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"`
|
||||
}
|
||||
|
||||
@@ -1694,6 +1706,10 @@ type SSHPrincipal struct {
|
||||
//
|
||||
// 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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -282,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() {
|
||||
@@ -299,14 +310,15 @@ func (m *Mon) debounce() {
|
||||
m.mu.Lock()
|
||||
|
||||
oldState := m.ifState
|
||||
changed := !curState.EqualFiltered(oldState, interfaces.UseInterestingInterfaces, interfaces.UseInterestingIPs)
|
||||
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))
|
||||
}
|
||||
}
|
||||
// See if we have a queued or new time jump signal.
|
||||
|
||||
@@ -112,11 +112,18 @@ 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 {
|
||||
case "llw", "awdl", "pdp_ip", "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) {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !android
|
||||
// +build !android
|
||||
|
||||
package monitor
|
||||
|
||||
import (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user