Compare commits

..

1 Commits

Author SHA1 Message Date
Brad Fitzpatrick
8b62f7beb8 ssh/tailssh: start moving auth checks earlier, adding banner
Updates #3802

Change-Id: I243952f10f439387dc56b01d03b13657ddf25069
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 18:25:19 -07:00
49 changed files with 516 additions and 1141 deletions

View File

@@ -1,53 +0,0 @@
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'

View File

@@ -117,32 +117,10 @@ 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.
### 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>
```
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.
## Building

View File

@@ -1,14 +0,0 @@
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

View File

@@ -1,19 +0,0 @@
#!/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

View File

@@ -1,8 +0,0 @@
#!/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

View File

@@ -4,28 +4,20 @@ set -e
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.1
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
--out tailscale-nginx-auth-0.1.0-amd64.deb \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=deb \
--version=0.1.0 \
--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
--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
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
--out tailscale-nginx-auth-0.1.0-amd64.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--version=0.1.0 \
--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,./README.md:/usr/share/tailscale/nginx-auth/README.md
--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

View File

@@ -17,7 +17,6 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
@@ -76,12 +75,6 @@ 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)

View File

@@ -1,9 +0,0 @@
# $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

View File

@@ -1,9 +0,0 @@
# $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

View File

@@ -86,12 +86,11 @@ func main() {
for i := 0; i < 60; i++ {
st, err := tailscale.Status(context.Background())
if err != nil {
log.Printf("error retrieving tailscale status; retrying: %v", err)
} else {
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
}
log.Fatal(err)
}
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
}
time.Sleep(time.Second)
}

View File

@@ -8,7 +8,6 @@ import (
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"flag"
@@ -22,11 +21,9 @@ 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"
)
@@ -109,11 +106,6 @@ var debugCmd = &ffcli.Command{
return fs
})(),
},
{
Name: "via",
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
},
}
@@ -356,46 +348,3 @@ 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
}

View File

@@ -538,17 +538,12 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
return tls.Client(nc, tlsConf)
}
// 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) {
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, err error) {
tcpConn, node, err := c.dialRegion(ctx, reg)
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
done := make(chan bool) // unbuffered
done := make(chan bool) // unbufferd
defer close(done)
tlsConn = c.tlsClient(tcpConn, node)
@@ -561,13 +556,13 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}()
err = tlsConn.Handshake()
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
select {
case done <- true:
return tlsConn, tcpConn, node, nil
return tlsConn, tcpConn, nil
case <-ctx.Done():
return nil, nil, nil, ctx.Err()
return nil, nil, ctx.Err()
}
}

View File

@@ -149,9 +149,3 @@ 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
View File

@@ -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-20220420224200-c602b5dfaa7f
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf
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-20220411220226-7b82a4e95df4
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
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
View File

@@ -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-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/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/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-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/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/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=

View File

@@ -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"},
{Addr: "8.8.8.8:53"},
},
Routes: map[dnsname.FQDN][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4"}},
"foo.com.": {{Addr: "1.2.3.4:53"}},
},
},
},
@@ -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"},
{Addr: "8.8.4.4:53"},
},
},
},

View File

@@ -344,7 +344,6 @@ 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()
@@ -1809,10 +1808,6 @@ 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
}
@@ -2212,7 +2207,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, r)
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
}
}
@@ -2241,7 +2236,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], r)
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
}
}
@@ -2272,6 +2267,16 @@ 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")
//

View File

@@ -386,14 +386,6 @@ 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
@@ -436,15 +428,8 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
return ln, nil
}
}
// 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
// Fall back to random ephemeral port.
return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
}
type peerAPIListener struct {
@@ -1062,49 +1047,3 @@ 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 }

View File

@@ -84,7 +84,10 @@ func (c Config) hasDefaultIPResolversOnly() bool {
return false
}
for _, r := range c.DefaultResolvers {
if ipp, ok := r.IPPort(); !ok || ipp.Port() != 53 {
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
continue
}
if _, err := netaddr.ParseIP(r.Addr); err != nil {
return false
}
}

View File

@@ -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.Resolver{Addr: ip.String()})
defaultRoutes = append(defaultRoutes, dnstype.ResolverFromIP(ip))
}
rcfg.Routes["."] = defaultRoutes
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
@@ -188,13 +188,23 @@ 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, ok := r.IPPort(); ok && ipp.Port() == 53 {
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && 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)
}

View File

@@ -96,7 +96,7 @@ func TestManager(t *testing.T) {
{
name: "corp",
in: Config{
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
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", "9.9.9.9"),
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
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", "9.9.9.9"),
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
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", "9.9.9.9"),
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
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", "9.9.9.9"),
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
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", "9.9.9.9"),
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
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", "9.9.9.9"),
Routes: upstreams("corp.com", "2.2.2.2"),
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
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", "9.9.9.9",
"corp.com.", "2.2.2.2"),
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
},
},
{
name: "corp-routes-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
Routes: upstreams("corp.com", "2.2.2.2"),
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
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", "9.9.9.9",
"corp.com.", "2.2.2.2"),
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
},
},
{
name: "routes",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
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",
"corp.com.", "2.2.2.2"),
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
},
},
{
name: "routes-split",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
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",
"bigco.net", "3.3.3.3"),
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
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",
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
},
},
{
name: "routes-multi-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
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",
"bigco.net.", "3.3.3.3"),
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
},
},
{
@@ -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"),
Routes: upstreams(".", "8.8.8.8:53"),
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", "ts.com", ""),
Routes: upstreams("corp.com", "2.2.2.2:53", "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",
".", "8.8.8.8"),
"corp.com.", "2.2.2.2:53",
".", "8.8.8.8:53"),
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",
"corp.com", "2.2.2.2:53",
"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"),
Routes: upstreams("corp.com.", "2.2.2.2:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -393,14 +393,6 @@ 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{
@@ -413,6 +405,8 @@ 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)
}
@@ -509,11 +503,6 @@ 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 {

View File

@@ -49,9 +49,9 @@ func TestDoH(t *testing.T) {
dohSem: make(chan struct{}, 10),
}
for urlBase := range publicdns.DoHIPsOfBase() {
t.Run(urlBase, func(t *testing.T) {
c, ok := f.getKnownDoHClientForProvider(urlBase)
for ip := range publicdns.KnownDoH() {
t.Run(ip.String(), func(t *testing.T) {
urlBase, c, ok := f.getKnownDoHClient(ip)
if !ok {
t.Fatal("expected DoH")
}

View File

@@ -25,7 +25,6 @@ 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"
@@ -50,11 +49,6 @@ 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).
@@ -234,55 +228,65 @@ 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, upgrading any well-known DoH (DNS-over-HTTP)
// servers in the process, insert a DoH lookup first before UDP fallbacks.
// 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.
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
}
done := map[hostAndFam]int{}
// 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 {
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
}
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
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
if host, ok := publicdns.KnownDoH()[ip]; ok {
total[hostAndFam{host, ip.BitLen()}]++
}
done[key]++
}
rr = append(rr, resolverAndDelay{
}
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()}]++
}
}
rr[i] = resolverAndDelay{
name: r,
startDelay: startDelay,
})
}
}
return rr
}
@@ -330,6 +334,23 @@ 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").
//
@@ -423,43 +444,34 @@ 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) (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)
}()
}
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
if strings.HasPrefix(rr.name.Addr, "http://") {
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
}
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("arbitrary https:// resolvers not supported yet")
return nil, fmt.Errorf("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, ok := rr.name.IPPort()
if !ok {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
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)
}
metricDNSFwdUDP.Add(1)
@@ -603,10 +615,6 @@ 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
@@ -647,8 +655,12 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
defer fq.closeOnCtxDone.Close()
resc := make(chan []byte, 1) // it's fine buffered or not
errc := make(chan error, 1) // it's fine buffered or not too
resc := make(chan []byte, 1)
var (
mu sync.Mutex
firstErr error
)
for i := range resolvers {
go func(rr *resolverAndDelay) {
if rr.startDelay > 0 {
@@ -662,48 +674,39 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
resb, err := f.send(ctx, fq, *rr)
if err != nil {
select {
case errc <- err:
case <-ctx.Done():
mu.Lock()
defer mu.Unlock()
if firstErr == nil {
firstErr = err
}
return
}
select {
case resc <- resb:
case <-ctx.Done():
default:
}
}(&resolvers[i])
}
var firstErr error
var numErr int
for {
select {
case v := <-resc:
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()
}
}

View File

@@ -7,6 +7,7 @@ package resolver
import (
"flag"
"fmt"
"net"
"reflect"
"strings"
"testing"
@@ -24,7 +25,11 @@ func (rr resolverAndDelay) String() string {
func TestResolversWithDelays(t *testing.T) {
// query
q := func(ss ...string) (ipps []dnstype.Resolver) {
for _, host := range ss {
for _, s := range ss {
host, _, err := net.SplitHostPort(s)
if err != nil {
t.Fatal(err)
}
ipps = append(ipps, dnstype.Resolver{Addr: host})
}
return
@@ -41,8 +46,12 @@ 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: s},
name: dnstype.Resolver{Addr: host},
startDelay: d,
})
}
@@ -56,28 +65,28 @@ func TestResolversWithDelays(t *testing.T) {
}{
{
name: "unknown-no-delays",
in: q("1.2.3.4", "2.3.4.5"),
want: o("1.2.3.4", "2.3.4.5"),
in: q("1.2.3.4:53", "2.3.4.5:53"),
want: o("1.2.3.4:53", "2.3.4.5:53"),
},
{
name: "google-all-ipv4",
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"),
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"),
},
{
name: "google-only-ipv6",
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"),
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"),
},
{
name: "google-all-four",
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"),
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"),
},
{
name: "quad9-one-v4-one-v6",
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"),
in: q("9.9.9.9:53", "[2620:fe::fe]:53"),
want: o("9.9.9.9:53", "[2620:fe::fe]:53+200ms"),
},
}

View File

@@ -725,22 +725,6 @@ 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
@@ -749,6 +733,8 @@ func (r *Resolver) fqdnForIPLocked(ip netaddr.IP, name dnsname.FQDN) (dnsname.FQ
return dnsSymbolicFQDN, dns.RCodeSuccess
}
r.mu.Lock()
defer r.mu.Unlock()
ret, ok := r.ipToHost[ip]
if !ok {
for _, suffix := range r.localDomains {

View File

@@ -382,7 +382,6 @@ 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 {
@@ -756,9 +755,6 @@ 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) {

View File

@@ -979,21 +979,13 @@ 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 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)
}
if _, err := http.DefaultClient.Do(req); err != nil {
c.logf("probing %s: %v", node.HostName, err)
return
}
t0 := c.timeNow()
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)
}
if _, err := http.DefaultClient.Do(req); err != nil {
c.logf("probing %s: %v", node.HostName, err)
return
}
d := c.timeNow().Sub(t0)
@@ -1013,7 +1005,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
var ip netaddr.IP
dc := derphttp.NewNetcheckClient(c.logf)
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
tlsConn, tcpConn, err := dc.DialRegionTLS(ctx, reg)
if err != nil {
return 0, ip, err
}
@@ -1044,7 +1036,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
}
hc := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, "GET", "https://" + node.HostName + "/derp/latency-check", nil)
req, err := http.NewRequestWithContext(ctx, "GET", "https://derp-unused-hostname.tld/derp/latency-check", nil)
if err != nil {
return 0, ip, err
}
@@ -1055,13 +1047,6 @@ 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

View File

@@ -2,9 +2,6 @@
// 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 (

View File

@@ -6,8 +6,6 @@
package tsaddr
import (
"encoding/binary"
"errors"
"sync"
"inet.af/netaddr"
@@ -128,18 +126,6 @@ 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)
@@ -294,17 +280,3 @@ 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
}

View File

@@ -41,11 +41,16 @@ 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)
ubuntu|pop|neon|zorin|elementary|linuxmint)
OS="ubuntu"
VERSION="$VERSION_CODENAME"
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
VERSION="$UBUNTU_CODENAME"
else
VERSION="$VERSION_CODENAME"
fi
PACKAGETYPE="apt"
# Third-party keyrings became the preferred method of
# installation in Ubuntu 20.04.
@@ -67,35 +72,6 @@ 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"
@@ -151,7 +127,7 @@ main() {
VERSION=""
PACKAGETYPE="dnf"
;;
rocky|almalinux)
rocky)
OS="fedora"
VERSION=""
PACKAGETYPE="dnf"
@@ -498,7 +474,7 @@ main() {
;;
emerge)
set -x
$SUDO emerge --ask=n net-vpn/tailscale
$SUDO emerge net-vpn/tailscale
set +x
;;
appstore)

View File

@@ -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.conn.srv.tailscaledPath == "" {
if ss.srv.tailscaledPath == "" {
return exec.CommandContext(ctx, name, args...)
}
lu := ss.conn.localUser
ci := ss.conn.info
lu := ss.localUser
ci := ss.connInfo
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.conn.srv.tailscaledPath, incubatorArgs...)
return exec.CommandContext(ctx, ss.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.conn.localUser.Uid)
shell := loginShell(ss.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.conn.info
ci := ss.connInfo
cmd := ss.newIncubatorCommand(ctx, shell, args)
cmd.Dir = ss.conn.localUser.HomeDir
cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
cmd.Dir = ss.localUser.HomeDir
cmd.Env = append(cmd.Env, envForUser(ss.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()),

View File

@@ -43,8 +43,8 @@ import (
)
var (
debugPolicyFile = envknob.SSHPolicyFile()
debugIgnoreTailnetSSHPolicy = envknob.SSHIgnoreTailnetPolicy()
debugPolicyFile = envknob.String("TS_DEBUG_SSH_POLICY_FILE")
debugIgnoreTailnetSSHPolicy = envknob.Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY")
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.newConn()
ss, err := srv.newSSHServer(c)
if err != nil {
return err
}
@@ -109,135 +109,51 @@ func (srv *server) OnPolicyChange() {
}
}
// 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),
func (srv *server) newSSHServer(c net.Conn) (*ssh.Server, error) {
ss := &ssh.Server{
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
conf := &gossh.ServerConfig{
NoClientAuth: true,
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
},
BannerCallback: func(m gossh.ConnMetadata) string {
// TODO(bradfitz): make this be a "you are rejected, contact
// your Tailnet admin etc etc" message or or the
// SSHAction.Message from the rejecting SSHAction if
// matched.
return "# Tailscale SSH server\n"
},
}
}
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
}
return conf
},
// 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,
Handler: srv.handleSSH,
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,
PublicKeyHandler: c.PublicKeyHandler,
ServerConfigCallback: c.ServerConfig,
// TODO(bradfitz,maisem): don't register this hook if the policy doesn't
// involve any pubkey stuff at all for the user identified by c.
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
},
}
ss := c.Server
for k, v := range ssh.DefaultRequestHandlers {
ss.RequestHandlers[k] = v
}
@@ -254,7 +170,7 @@ func (srv *server) newConn() (*conn, error) {
for _, signer := range keys {
ss.AddHostKey(signer)
}
return c, nil
return ss, nil
}
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
@@ -268,24 +184,37 @@ func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string
return ss.action.AllowLocalPortForwarding
}
// 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()
// 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()
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 c.ruleExpired(r) {
if ci.ruleExpired(r) {
continue
}
if mapLocalUser(r.SSHUsers, ci.sshUser) == "" {
if mapLocalUser(r.SSHUsers, sshUser) == "" {
continue
}
for _, p := range r.Principals {
if len(p.PubKeys) > 0 && c.principalMatchesTailscaleIdentity(p) {
if principalMatchesTailscaleIdentity(p, ci) && len(p.PubKeys) > 0 {
return true
}
}
@@ -293,11 +222,19 @@ func (c *conn) havePubKeyPolicy(ci *sshConnInfo) bool {
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 (c *conn) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
lb := c.srv.lb
func (srv *server) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
lb := srv.lb
nm := lb.NetMap()
if nm == nil {
return nil, false
@@ -306,15 +243,14 @@ func (c *conn) 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 {
c.logf("error reading debug SSH policy file: %v", err)
srv.logf("error reading debug SSH policy file: %v", err)
return nil, false
}
p := new(tailcfg.SSHPolicy)
if err := json.Unmarshal(f, p); err != nil {
c.logf("invalid JSON in %v: %v", debugPolicyFile, err)
srv.logf("invalid JSON in %v: %v", debugPolicyFile, err)
return nil, false
}
return p, true
@@ -334,45 +270,42 @@ 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 pubKey may be nil for "none"
// auth.
func (c *conn) evaluatePolicy(pubKey gossh.PublicKey) (_ *tailcfg.SSHAction, localUser string, _ error) {
pol, ok := c.sshPolicy()
// 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()
if !ok {
return nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
return nil, nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
}
a, localUser, ok := c.evalSSHPolicy(pol, pubKey)
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)
if !ok {
return nil, "", fmt.Errorf("tailssh: rejecting connection; no matching policy")
return nil, nil, "", fmt.Errorf("unknown Tailscale identity from src %v", remoteAddr)
}
return a, localUser, nil
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
}
// pubKeyCacheEntry is the cache value for an HTTPS URL of public keys (like
@@ -418,9 +351,6 @@ 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")
@@ -473,38 +403,52 @@ func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
return lines, err
}
// 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) {
// handleSSH is invoked when a new SSH connection attempt is made.
func (srv *server) handleSSH(s ssh.Session) {
logf := srv.logf
sshUser := s.User()
action, err := c.resolveTerminalAction(s)
action, ci, localUser, err := srv.evaluatePolicy(sshUser, toIPPort(s.LocalAddr()), toIPPort(s.RemoteAddr()), s.PublicKey())
if err != nil {
c.logf("resolveTerminalAction: %v", err)
io.WriteString(s.Stderr(), "Access Denied: failed during authorization check.\r\n")
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")
s.Exit(1)
return
}
if action.Reject || !action.Accept {
c.logf("access denied for %v", c.info.uprof.LoginName)
ss.logf("access denied for %v (%v)", ci.uprof.LoginName, ci.src.IP())
s.Exit(1)
return
}
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.logf("access granted for %v (%v) to ssh-user %q", ci.uprof.LoginName, ci.src.IP(), sshUser)
ss.action = action
ss.run()
}
// resolveTerminalAction either returns action0 (if it's Accept or Reject) or
// else loops, fetching new SSHActions from the control plane.
// resolveTerminalAction either returns action (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 s.
// Any action with a Message in the chain will be printed to ss.
//
// The returned SSHAction will be either Reject or Accept.
func (c *conn) resolveTerminalAction(s ssh.Session) (*tailcfg.SSHAction, error) {
action := c.action0
func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg.SSHAction, error) {
// 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
@@ -513,7 +457,7 @@ func (c *conn) resolveTerminalAction(s ssh.Session) (*tailcfg.SSHAction, error)
// instructions and go to a URL to do something.)
for {
if action.Message != "" {
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
io.WriteString(ss.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
}
if action.Accept || action.Reject {
return action, nil
@@ -522,48 +466,31 @@ func (c *conn) resolveTerminalAction(s ssh.Session) (*tailcfg.SSHAction, error)
if url == "" {
return nil, errors.New("reached Action that lacked Accept, Reject, and HoldAndDelegate")
}
url = c.expandDelegateURL(url)
url = ss.expandDelegateURL(url)
var err error
action, err = c.fetchSSHAction(s.Context(), url)
action, err = ss.srv.fetchSSHAction(ss.Context(), url)
if err != nil {
return nil, fmt.Errorf("fetching SSHAction from %s: %w", url, err)
}
}
}
func (c *conn) expandDelegateURL(actionURL string) string {
nm := c.srv.lb.NetMap()
ci := c.info
func (ss *sshSession) expandDelegateURL(actionURL string) string {
nm := ss.srv.lb.NetMap()
var dstNodeID string
if nm != nil {
dstNodeID = fmt.Sprint(int64(nm.SelfNode.ID))
}
return strings.NewReplacer(
"$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()),
"$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()),
"$DST_NODE_ID", dstNodeID,
"$SSH_USER", url.QueryEscape(ci.sshUser),
"$LOCAL_USER", url.QueryEscape(c.localUser.Username),
"$SSH_USER", url.QueryEscape(ss.connInfo.sshUser),
"$LOCAL_USER", url.QueryEscape(ss.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
@@ -572,8 +499,10 @@ type sshSession struct {
logf logger.Logf
ctx *sshContext // implements context.Context
conn *conn
srv *server
connInfo *sshConnInfo
action *tailcfg.SSHAction
localUser *user.User
agentListener net.Listener // non-nil if agent-forwarding requested+allowed
// initialized by launchProcess:
@@ -594,48 +523,39 @@ func (ss *sshSession) vlogf(format string, args ...interface{}) {
}
}
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)
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))
return &sshSession{
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,
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+"): "),
}
}
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() {
if ss.conn.isStillValid(ss.PublicKey()) {
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 {
return
}
ss.logf("session no longer valid per new SSH policy; closing")
ss.ctx.CloseWithError(userVisibleError{
fmt.Sprintf("Access revoked.\r\n"),
fmt.Sprintf("Access revoked.\n"),
context.Canceled,
})
}
func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
func (srv *server) 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", c.logf, 10*time.Second)
bo := backoff.NewBackoff("fetch-ssh-action", srv.logf, 10*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, err
@@ -644,7 +564,7 @@ func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHActi
if err != nil {
return nil, err
}
res, err := c.srv.lb.DoNoiseRequest(req)
res, err := srv.lb.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
@@ -655,7 +575,7 @@ func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHActi
if len(body) > 1<<10 {
body = body[:1<<10]
}
c.logf("fetch of %v: %s, %s", url, res.Status, body)
srv.logf("fetch of %v: %s, %s", url, res.Status, body)
bo.BackOff(ctx, fmt.Errorf("unexpected status: %v", res.Status))
continue
}
@@ -663,7 +583,7 @@ func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHActi
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
c.logf("invalid next SSHAction JSON from %v: %v", url, err)
srv.logf("invalid next SSHAction JSON from %v: %v", url, err)
bo.BackOff(ctx, err)
continue
}
@@ -685,7 +605,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.conn.info.src.IP(), err)
ss.logf("terminating SSH session from %v: %v", ss.connInfo.src.IP(), err)
ss.cmd.Process.Kill()
})
}
@@ -780,7 +700,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.conn.srv
srv := ss.srv
srv.startSession(ss)
defer srv.endSession(ss)
@@ -797,13 +717,13 @@ func (ss *sshSession) run() {
}
logf := srv.logf
lu := ss.conn.localUser
lu := ss.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\r\n")
fmt.Fprintf(ss, "can't switch user\n")
ss.Exit(1)
return
}
@@ -825,7 +745,7 @@ func (ss *sshSession) run() {
var err error
rec, err = ss.startNewRecording()
if err != nil {
fmt.Fprintf(ss, "can't start new recording\r\n")
fmt.Fprintf(ss, "can't start new recording\n")
ss.logf("startNewRecording: %v", err)
ss.Exit(1)
return
@@ -898,6 +818,14 @@ 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
@@ -912,22 +840,23 @@ 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) String() string {
return fmt.Sprintf("%v->%v@%v", ci.src, ci.sshUser, ci.dst)
}
func (c *conn) ruleExpired(r *tailcfg.SSHRule) bool {
func (ci *sshConnInfo) ruleExpired(r *tailcfg.SSHRule) bool {
if r.RuleExpires == nil {
return false
}
return r.RuleExpires.Before(c.now)
return r.RuleExpires.Before(ci.now)
}
func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, ok bool) {
func evalSSHPolicy(pol *tailcfg.SSHPolicy, ci *sshConnInfo) (a *tailcfg.SSHAction, localUser string, ok bool) {
for _, r := range pol.Rules {
if a, localUser, err := c.matchRule(r, pubKey); err == nil {
if a, localUser, err := matchRule(r, ci); err == nil {
return a, localUser, true
}
}
@@ -943,25 +872,23 @@ var (
errUserMatch = errors.New("user didn't match")
)
func (c *conn) matchRule(r *tailcfg.SSHRule, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, err error) {
func matchRule(r *tailcfg.SSHRule, ci *sshConnInfo) (a *tailcfg.SSHAction, localUser string, err error) {
if r == nil {
return nil, "", errNilRule
}
if r.Action == nil {
return nil, "", errNilAction
}
if c.ruleExpired(r) {
if ci.ruleExpired(r) {
return nil, "", errRuleExpired
}
if !r.Action.Reject || r.SSHUsers != nil {
localUser = mapLocalUser(r.SSHUsers, c.info.sshUser)
localUser = mapLocalUser(r.SSHUsers, ci.sshUser)
if localUser == "" {
return nil, "", errUserMatch
}
}
if ok, err := c.anyPrincipalMatches(r.Principals, pubKey); err != nil {
return nil, "", err
} else if !ok {
if !anyPrincipalMatches(r.Principals, ci) {
return nil, "", errPrincipalMatch
}
return r.Action, localUser, nil
@@ -978,32 +905,27 @@ func mapLocalUser(ruleSSHUsers map[string]string, reqSSHUser string) (localUser
return v
}
func (c *conn) anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
func anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
for _, p := range ps {
if p == nil {
continue
}
if ok, err := c.principalMatches(p, pubKey); err != nil {
return false, err
} else if ok {
return true, nil
if principalMatches(p, ci) {
return true
}
}
return false, nil
return false
}
func (c *conn) principalMatches(p *tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
if !c.principalMatchesTailscaleIdentity(p) {
return false, nil
}
return c.principalMatchesPubKey(p, pubKey)
func principalMatches(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
return principalMatchesTailscaleIdentity(p, ci) &&
principalMatchesPubKey(p, ci)
}
// 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 (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
ci := c.info
func principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
if p.Any {
return true
}
@@ -1021,27 +943,32 @@ func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
return false
}
func (c *conn) principalMatchesPubKey(p *tailcfg.SSHPrincipal, clientPubKey gossh.PublicKey) (bool, error) {
func principalMatchesPubKey(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
if len(p.PubKeys) == 0 {
return true, nil
return true
}
if clientPubKey == nil {
return false, nil
if ci.pubKey == nil {
return false
}
knownKeys := p.PubKeys
if len(knownKeys) == 1 && strings.HasPrefix(knownKeys[0], "https://") {
pubKeys := p.PubKeys
if len(pubKeys) == 1 && strings.HasPrefix(pubKeys[0], "https://") {
if ci.fetchPublicKeysURL == nil {
// TODO: log?
return false
}
var err error
knownKeys, err = c.srv.fetchPublicKeysURL(c.expandPublicKeyURL(knownKeys[0]))
pubKeys, err = ci.fetchPublicKeysURL(pubKeys[0])
if err != nil {
return false, err
// TODO: log?
return false
}
}
for _, knownKey := range knownKeys {
if pubKeyMatchesAuthorizedKey(clientPubKey, knownKey) {
return true, nil
for _, pubKey := range pubKeys {
if pubKeyMatchesAuthorizedKey(ci.pubKey, pubKey) {
return true
}
}
return false, nil
return false
}
func pubKeyMatchesAuthorizedKey(pubKey ssh.PublicKey, wantKey string) bool {
@@ -1085,7 +1012,7 @@ func (ss *sshSession) startNewRecording() (*recording, error) {
ss: ss,
start: now,
}
varRoot := ss.conn.srv.lb.TailscaleVarRoot()
varRoot := ss.srv.lb.TailscaleVarRoot()
if varRoot == "" {
return nil, errors.New("no var root for recording storage")
}

View File

@@ -63,7 +63,7 @@ func TestMatchRule(t *testing.T) {
Action: someAction,
RuleExpires: timePtr(time.Unix(100, 0)),
},
ci: &sshConnInfo{},
ci: &sshConnInfo{now: time.Unix(200, 0)},
wantErr: errRuleExpired,
},
{
@@ -178,11 +178,7 @@ func TestMatchRule(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &conn{
now: time.Unix(200, 0),
info: tt.ci,
}
got, gotUser, err := c.matchRule(tt.rule, nil)
got, gotUser, err := matchRule(tt.rule, tt.ci)
if err != tt.wantErr {
t.Errorf("err = %v; want %v", err, tt.wantErr)
}
@@ -215,33 +211,10 @@ func TestSSH(t *testing.T) {
dir := t.TempDir()
lb.SetVarRoot(dir)
srv := &server{
lb: lb,
logf: logf,
}
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)
}
sc.localUser = u
sc.info = &sshConnInfo{
sshUser: "test",
src: netaddr.MustParseIPPort("1.2.3.4:32342"),
dst: netaddr.MustParseIPPort("1.2.3.5:22"),
node: &tailcfg.Node{},
uprof: &tailcfg.UserProfile{},
}
sc.Handler = func(s ssh.Session) {
sc.newSSHSession(s, &tailcfg.SSHAction{Accept: true}).run()
}
ln, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
@@ -259,13 +232,36 @@ func TestSSH(t *testing.T) {
}
return
}
go sc.HandleConn(c)
srv := &server{
lb: lb,
logf: logf,
}
ss, err := srv.newSSHServer(c)
if err != nil {
t.Error(err)
return
}
ci := &sshConnInfo{
sshUser: "test",
src: netaddr.MustParseIPPort("1.2.3.4:32342"),
dst: netaddr.MustParseIPPort("1.2.3.5:22"),
node: &tailcfg.Node{},
uprof: &tailcfg.UserProfile{},
}
ss.Handler = func(s ssh.Session) {
ss := srv.newSSHSession(s, ci, u)
ss.action = &tailcfg.SSHAction{Accept: true}
ss.run()
}
go ss.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")
@@ -281,7 +277,7 @@ func TestSSH(t *testing.T) {
cmd.Env = append(os.Environ(), "LOCAL_ENV=bar")
got, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err, string(got))
t.Fatal(err)
}
m := parseEnv(got)
if got := m["USER"]; got == "" || got != u.Username {
@@ -408,26 +404,3 @@ 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)
}
}

View File

@@ -1636,21 +1636,9 @@ 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.
//
// 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 are the rules to process for an incoming SSH
// connection. The first matching rule takes its action and
// stops processing further rules.
Rules []*SSHRule `json:"rules"`
}
@@ -1706,10 +1694,6 @@ 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"`
}

View File

@@ -1,7 +1,6 @@
package ssh_test
import (
"errors"
"io"
"io/ioutil"
@@ -28,19 +27,10 @@ func ExampleNoPty() {
func ExamplePublicKeyAuth() {
ssh.ListenAndServe(":2222", nil,
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
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)
}),
)
}

View File

@@ -144,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 err := srv.PublicKeyHandler(ctx, key); err != nil {
return ctx.Permissions().Permissions, err
if ok := srv.PublicKeyHandler(ctx, key); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
}
ctx.SetValue(ContextKeyPublicKey, key)
return ctx.Permissions().Permissions, nil

View File

@@ -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) error
type PublicKeyHandler func(ctx Context, key PublicKey) bool
// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool

View File

@@ -1,62 +0,0 @@
// 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"
)

View File

@@ -1,42 +0,0 @@
// 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))
}

View File

@@ -12,9 +12,7 @@ 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.
// This is the common format as sent by the control plane.
// - An IP:port, for tests.
// - A plain IP address for a "classic" UDP+TCP DNS resolver
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
// - [TODO] "https://resolver.com/query-tmpl" for DNS over HTTPS
Addr string `json:",omitempty"`
@@ -28,20 +26,7 @@ type Resolver struct {
BootstrapResolution []netaddr.IP `json:",omitempty"`
}
// 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
// ResolverFromIP defines a Resolver for ip on port 53.
func ResolverFromIP(ip netaddr.IP) Resolver {
return Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()}
}

View File

@@ -42,10 +42,6 @@ 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
@@ -286,13 +282,6 @@ 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() {
@@ -310,15 +299,14 @@ func (m *Mon) debounce() {
m.mu.Lock()
oldState := m.ifState
changed := !curState.EqualFiltered(oldState, m.isInterestingInterface, interfaces.UseInterestingIPs)
changed := !curState.EqualFiltered(oldState, interfaces.UseInterestingInterfaces, 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", s1)
m.logf("[unexpected] old: %s", jsonSummary(oldState))
m.logf("[unexpected] new: %s", jsonSummary(curState))
m.logf("[unexpected] network state changed, but stringification didn't: %v\nold: %s\nnew: %s\n", s1,
jsonSummary(oldState), jsonSummary(curState))
}
}
// See if we have a queued or new time jump signal.

View File

@@ -112,18 +112,11 @@ 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 {
if !m.IsInterestingInterface(la.Name) {
baseName := strings.TrimRight(la.Name, "0123456789")
switch baseName {
case "llw", "awdl", "pdp_ip", "ipsec":
return true
}
}

View File

@@ -34,8 +34,6 @@ 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()
}

View File

@@ -64,8 +64,6 @@ 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) {

View File

@@ -2,9 +2,6 @@
// 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 (

View File

@@ -72,8 +72,6 @@ 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()

View File

@@ -38,10 +38,6 @@ 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)

View File

@@ -1514,14 +1514,8 @@ func supportsV6NAT() bool {
// Can't read the file. Assume SNAT works.
return true
}
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
return bytes.Contains(bs, []byte("nat\n"))
}
func checkIPRuleSupportsV6(logf logger.Logf) error {