Compare commits
1 Commits
bradfitz/l
...
bradfitz/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a794630f60 |
28
.github/workflows/checklocks.yml
vendored
28
.github/workflows/checklocks.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: checklocks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.go'
|
||||
- '.github/workflows/checklocks.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
checklocks:
|
||||
runs-on: [ ubuntu-latest ]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build checklocks
|
||||
run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks
|
||||
|
||||
- name: Run checklocks vet
|
||||
# TODO: remove || true once we have applied checklocks annotations everywhere.
|
||||
run: ./tool/go vet -vettool=/tmp/checklocks ./... || true
|
||||
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -31,10 +31,10 @@ jobs:
|
||||
cache: false
|
||||
|
||||
- name: golangci-lint
|
||||
# Note: this is the 'v3' tag as of 2023-08-14
|
||||
# Note: this is the 'v3' tag as of 2023-04-17
|
||||
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
|
||||
with:
|
||||
version: v1.54.2
|
||||
version: v1.52.2
|
||||
|
||||
# Show only new issues if it's a pull request.
|
||||
only-new-issues: true
|
||||
|
||||
44
.github/workflows/test.yml
vendored
44
.github/workflows/test.yml
vendored
@@ -22,7 +22,8 @@ on:
|
||||
- "main"
|
||||
- "release-branch/*"
|
||||
pull_request:
|
||||
# all PRs on all branches
|
||||
branches:
|
||||
- "*"
|
||||
merge_group:
|
||||
branches:
|
||||
- "main"
|
||||
@@ -38,26 +39,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
race-root-integration:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false # don't abort the entire matrix if one element fails
|
||||
matrix:
|
||||
include:
|
||||
- shard: '1/4'
|
||||
- shard: '2/4'
|
||||
- shard: '3/4'
|
||||
- shard: '4/4'
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
- name: integration tests as root
|
||||
run: PATH=$PWD/tool:$PATH /tmp/testwrapper -exec "sudo -E" -race ./tstest/integration/
|
||||
env:
|
||||
TS_TEST_SHARD: ${{ matrix.shard }}
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false # don't abort the entire matrix if one element fails
|
||||
@@ -66,13 +47,6 @@ jobs:
|
||||
- goarch: amd64
|
||||
- goarch: amd64
|
||||
buildflags: "-race"
|
||||
shard: '1/3'
|
||||
- goarch: amd64
|
||||
buildflags: "-race"
|
||||
shard: '2/3'
|
||||
- goarch: amd64
|
||||
buildflags: "-race"
|
||||
shard: '3/3'
|
||||
- goarch: "386" # thanks yaml
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
@@ -96,7 +70,6 @@ jobs:
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
|
||||
- name: build all
|
||||
if: matrix.buildflags == '' # skip on race builder
|
||||
run: ./tool/go build ${{matrix.buildflags}} ./...
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
@@ -121,7 +94,6 @@ jobs:
|
||||
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
TS_TEST_SHARD: ${{ matrix.shard }}
|
||||
- name: bench all
|
||||
run: ./tool/go test ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$ $(for x in $(git grep -l "^func Benchmark" | xargs dirname | sort | uniq); do echo "./$x"; done)
|
||||
env:
|
||||
@@ -190,17 +162,7 @@ jobs:
|
||||
HOME: "/tmp"
|
||||
TMPDIR: "/tmp"
|
||||
XDB_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
race-build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build all
|
||||
run: ./tool/go install -race ./cmd/...
|
||||
- name: build tests
|
||||
run: ./tool/go test -race -exec=true ./...
|
||||
|
||||
|
||||
cross: # cross-compile checks, build only.
|
||||
strategy:
|
||||
fail-fast: false # don't abort the entire matrix if one element fails
|
||||
|
||||
4
api.md
4
api.md
@@ -209,6 +209,10 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
|
||||
"192.68.0.21:59128"
|
||||
],
|
||||
|
||||
// derp (string) is the IP:port of the DERP server currently being used.
|
||||
// Learn about DERP servers at https://tailscale.com/kb/1232/.
|
||||
"derp":"",
|
||||
|
||||
// mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings
|
||||
// vary based on the destination IP.
|
||||
"mappingVariesByDestIP":false,
|
||||
|
||||
328
appc/appc.go
328
appc/appc.go
@@ -1,328 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package appc implements App Connectors.
|
||||
package appc
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
|
||||
|
||||
// target describes the predicates which route some inbound
|
||||
// traffic to the app connector to a specific handler.
|
||||
type target struct {
|
||||
Dest netip.Prefix
|
||||
Matching tailcfg.ProtoPortRange
|
||||
}
|
||||
|
||||
// Server implements an App Connector.
|
||||
type Server struct {
|
||||
mu sync.RWMutex // mu guards following fields
|
||||
connectors map[appctype.ConfigID]connector
|
||||
}
|
||||
|
||||
type appcMetrics struct {
|
||||
dnsResponses expvar.Int
|
||||
dnsFailures expvar.Int
|
||||
tcpConns expvar.Int
|
||||
sniConns expvar.Int
|
||||
unhandledConns expvar.Int
|
||||
}
|
||||
|
||||
var getMetrics = sync.OnceValue[*appcMetrics](func() *appcMetrics {
|
||||
m := appcMetrics{}
|
||||
|
||||
stats := new(metrics.Set)
|
||||
stats.Set("tls_sessions", &m.sniConns)
|
||||
clientmetric.NewCounterFunc("sniproxy_tls_sessions", m.sniConns.Value)
|
||||
stats.Set("tcp_sessions", &m.tcpConns)
|
||||
clientmetric.NewCounterFunc("sniproxy_tcp_sessions", m.tcpConns.Value)
|
||||
stats.Set("dns_responses", &m.dnsResponses)
|
||||
clientmetric.NewCounterFunc("sniproxy_dns_responses", m.dnsResponses.Value)
|
||||
stats.Set("dns_failed", &m.dnsFailures)
|
||||
clientmetric.NewCounterFunc("sniproxy_dns_failed", m.dnsFailures.Value)
|
||||
expvar.Publish("sniproxy", stats)
|
||||
|
||||
return &m
|
||||
})
|
||||
|
||||
// Configure applies the provided configuration to the app connector.
|
||||
func (s *Server) Configure(cfg *appctype.AppConnectorConfig) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.connectors = makeConnectorsFromConfig(cfg)
|
||||
}
|
||||
|
||||
// HandleTCPFlow implements tsnet.FallbackTCPHandler.
|
||||
func (s *Server) HandleTCPFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
|
||||
m := getMetrics()
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, c := range s.connectors {
|
||||
if handler, intercept := c.handleTCPFlow(src, dst, m); intercept {
|
||||
return handler, intercept
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// HandleDNS handles a DNS request to the app connector.
|
||||
func (s *Server) HandleDNS(c nettype.ConnPacketConn) {
|
||||
defer c.Close()
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
m := getMetrics()
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
n, err := c.Read(buf)
|
||||
if err != nil {
|
||||
log.Printf("HandleDNS: read failed: %v\n ", err)
|
||||
m.dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
addrPortStr := c.LocalAddr().String()
|
||||
host, _, err := net.SplitHostPort(addrPortStr)
|
||||
if err != nil {
|
||||
log.Printf("HandleDNS: bogus addrPort %q", addrPortStr)
|
||||
m.dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
localAddr, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
log.Printf("HandleDNS: bogus local address %q", host)
|
||||
m.dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
var msg dnsmessage.Message
|
||||
err = msg.Unpack(buf[:n])
|
||||
if err != nil {
|
||||
log.Printf("HandleDNS: dnsmessage unpack failed: %v\n ", err)
|
||||
m.dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, connector := range s.connectors {
|
||||
resp, err := connector.handleDNS(&msg, localAddr)
|
||||
if err != nil {
|
||||
log.Printf("HandleDNS: connector handling failed: %v\n", err)
|
||||
m.dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
if len(resp) > 0 {
|
||||
// This connector handled the DNS request
|
||||
_, err = c.Write(resp)
|
||||
if err != nil {
|
||||
log.Printf("HandleDNS: write failed: %v\n", err)
|
||||
m.dnsFailures.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
m.dnsResponses.Add(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connector describes a logical collection of
|
||||
// services which need to be proxied.
|
||||
type connector struct {
|
||||
Handlers map[target]handler
|
||||
}
|
||||
|
||||
// handleTCPFlow implements tsnet.FallbackTCPHandler.
|
||||
func (c *connector) handleTCPFlow(src, dst netip.AddrPort, m *appcMetrics) (handler func(net.Conn), intercept bool) {
|
||||
for t, h := range c.Handlers {
|
||||
if t.Matching.Proto != 0 && t.Matching.Proto != int(ipproto.TCP) {
|
||||
continue
|
||||
}
|
||||
if !t.Dest.Contains(dst.Addr()) {
|
||||
continue
|
||||
}
|
||||
if !t.Matching.Ports.Contains(dst.Port()) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.(type) {
|
||||
case *tcpSNIHandler:
|
||||
m.sniConns.Add(1)
|
||||
case *tcpRoundRobinHandler:
|
||||
m.tcpConns.Add(1)
|
||||
default:
|
||||
log.Printf("handleTCPFlow: unhandled handler type %T", h)
|
||||
}
|
||||
|
||||
return h.Handle, true
|
||||
}
|
||||
|
||||
m.unhandledConns.Add(1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// handleDNS returns the DNS response to the given query. If this
|
||||
// connector is unable to handle the request, nil is returned.
|
||||
func (c *connector) handleDNS(req *dnsmessage.Message, localAddr netip.Addr) (response []byte, err error) {
|
||||
for t, h := range c.Handlers {
|
||||
if t.Dest.Contains(localAddr) {
|
||||
return makeDNSResponse(req, h.ReachableOn())
|
||||
}
|
||||
}
|
||||
|
||||
// Did not match, signal 'not handled' to caller
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func makeDNSResponse(req *dnsmessage.Message, reachableIPs []netip.Addr) (response []byte, err error) {
|
||||
buf := make([]byte, 1500)
|
||||
resp := dnsmessage.NewBuilder(buf,
|
||||
dnsmessage.Header{
|
||||
ID: req.Header.ID,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
})
|
||||
resp.EnableCompression()
|
||||
|
||||
if len(req.Questions) == 0 {
|
||||
buf, _ = resp.Finish()
|
||||
return buf, nil
|
||||
}
|
||||
q := req.Questions[0]
|
||||
err = resp.StartQuestions()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Question(q)
|
||||
|
||||
err = resp.StartAnswers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch q.Type {
|
||||
case dnsmessage.TypeAAAA:
|
||||
for _, ip := range reachableIPs {
|
||||
if ip.Is6() {
|
||||
err = resp.AAAAResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.AAAAResource{AAAA: ip.As16()},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case dnsmessage.TypeA:
|
||||
for _, ip := range reachableIPs {
|
||||
if ip.Is4() {
|
||||
err = resp.AResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.AResource{A: ip.As4()},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case dnsmessage.TypeSOA:
|
||||
err = resp.SOAResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
|
||||
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
|
||||
)
|
||||
case dnsmessage.TypeNS:
|
||||
err = resp.NSResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.NSResource{NS: tsMBox},
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Finish()
|
||||
}
|
||||
|
||||
type handler interface {
|
||||
// Handle handles the given socket.
|
||||
Handle(c net.Conn)
|
||||
|
||||
// ReachableOn returns the IP addresses this handler is reachable on.
|
||||
ReachableOn() []netip.Addr
|
||||
}
|
||||
|
||||
func installDNATHandler(d *appctype.DNATConfig, out *connector) {
|
||||
// These handlers don't actually do DNAT, they just
|
||||
// proxy the data over the connection.
|
||||
var dialer net.Dialer
|
||||
dialer.Timeout = 5 * time.Second
|
||||
h := tcpRoundRobinHandler{
|
||||
To: d.To,
|
||||
DialContext: dialer.DialContext,
|
||||
ReachableIPs: d.Addrs,
|
||||
}
|
||||
|
||||
for _, addr := range d.Addrs {
|
||||
for _, protoPort := range d.IP {
|
||||
t := target{
|
||||
Dest: netip.PrefixFrom(addr, addr.BitLen()),
|
||||
Matching: protoPort,
|
||||
}
|
||||
|
||||
mak.Set(&out.Handlers, t, handler(&h))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installSNIHandler(c *appctype.SNIProxyConfig, out *connector) {
|
||||
var dialer net.Dialer
|
||||
dialer.Timeout = 5 * time.Second
|
||||
h := tcpSNIHandler{
|
||||
Allowlist: c.AllowedDomains,
|
||||
DialContext: dialer.DialContext,
|
||||
ReachableIPs: c.Addrs,
|
||||
}
|
||||
|
||||
for _, addr := range c.Addrs {
|
||||
for _, protoPort := range c.IP {
|
||||
t := target{
|
||||
Dest: netip.PrefixFrom(addr, addr.BitLen()),
|
||||
Matching: protoPort,
|
||||
}
|
||||
|
||||
mak.Set(&out.Handlers, t, handler(&h))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeConnectorsFromConfig(cfg *appctype.AppConnectorConfig) map[appctype.ConfigID]connector {
|
||||
var connectors map[appctype.ConfigID]connector
|
||||
|
||||
for cID, d := range cfg.DNAT {
|
||||
c := connectors[cID]
|
||||
installDNATHandler(&d, &c)
|
||||
mak.Set(&connectors, cID, c)
|
||||
}
|
||||
for cID, d := range cfg.SNIProxy {
|
||||
c := connectors[cID]
|
||||
installSNIHandler(&d, &c)
|
||||
mak.Set(&connectors, cID, c)
|
||||
}
|
||||
|
||||
return connectors
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
)
|
||||
|
||||
func TestMakeConnectorsFromConfig(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
input *appctype.AppConnectorConfig
|
||||
want map[appctype.ConfigID]connector
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
&appctype.AppConnectorConfig{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"DNAT",
|
||||
&appctype.AppConnectorConfig{
|
||||
DNAT: map[appctype.ConfigID]appctype.DNATConfig{
|
||||
"swiggity_swooty": {
|
||||
Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
|
||||
To: []string{"example.org"},
|
||||
IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[appctype.ConfigID]connector{
|
||||
"swiggity_swooty": {
|
||||
Handlers: map[target]handler{
|
||||
{
|
||||
Dest: netip.MustParsePrefix("100.64.0.1/32"),
|
||||
Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
|
||||
}: &tcpRoundRobinHandler{To: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
|
||||
{
|
||||
Dest: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
|
||||
}: &tcpRoundRobinHandler{To: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"SNIProxy",
|
||||
&appctype.AppConnectorConfig{
|
||||
SNIProxy: map[appctype.ConfigID]appctype.SNIProxyConfig{
|
||||
"swiggity_swooty": {
|
||||
Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
|
||||
AllowedDomains: []string{"example.org"},
|
||||
IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[appctype.ConfigID]connector{
|
||||
"swiggity_swooty": {
|
||||
Handlers: map[target]handler{
|
||||
{
|
||||
Dest: netip.MustParsePrefix("100.64.0.1/32"),
|
||||
Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
|
||||
}: &tcpSNIHandler{Allowlist: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
|
||||
{
|
||||
Dest: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
|
||||
}: &tcpSNIHandler{Allowlist: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
connectors := makeConnectorsFromConfig(tc.input)
|
||||
|
||||
if diff := cmp.Diff(connectors, tc.want,
|
||||
cmpopts.IgnoreFields(tcpRoundRobinHandler{}, "DialContext"),
|
||||
cmpopts.IgnoreFields(tcpSNIHandler{}, "DialContext"),
|
||||
cmp.Comparer(func(x, y netip.Addr) bool {
|
||||
return x == y
|
||||
})); diff != "" {
|
||||
t.Fatalf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
104
appc/handlers.go
104
appc/handlers.go
@@ -1,104 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"inet.af/tcpproxy"
|
||||
"tailscale.com/net/netutil"
|
||||
)
|
||||
|
||||
type tcpRoundRobinHandler struct {
|
||||
// To is a list of destination addresses to forward to.
|
||||
// An entry may be either an IP address or a DNS name.
|
||||
To []string
|
||||
|
||||
// DialContext is used to make the outgoing TCP connection.
|
||||
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// ReachableIPs enumerates the IP addresses this handler is reachable on.
|
||||
ReachableIPs []netip.Addr
|
||||
}
|
||||
|
||||
// ReachableOn returns the IP addresses this handler is reachable on.
|
||||
func (h *tcpRoundRobinHandler) ReachableOn() []netip.Addr {
|
||||
return h.ReachableIPs
|
||||
}
|
||||
|
||||
func (h *tcpRoundRobinHandler) Handle(c net.Conn) {
|
||||
addrPortStr := c.LocalAddr().String()
|
||||
_, port, err := net.SplitHostPort(addrPortStr)
|
||||
if err != nil {
|
||||
log.Printf("tcpRoundRobinHandler.Handle: bogus addrPort %q", addrPortStr)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var p tcpproxy.Proxy
|
||||
p.ListenFunc = func(net, laddr string) (net.Listener, error) {
|
||||
return netutil.NewOneConnListener(c, nil), nil
|
||||
}
|
||||
|
||||
dest := h.To[rand.Intn(len(h.To))]
|
||||
dial := &tcpproxy.DialProxy{
|
||||
Addr: fmt.Sprintf("%s:%s", dest, port),
|
||||
DialContext: h.DialContext,
|
||||
}
|
||||
|
||||
p.AddRoute(addrPortStr, dial)
|
||||
p.Start()
|
||||
}
|
||||
|
||||
type tcpSNIHandler struct {
|
||||
// Allowlist enumerates the FQDNs which may be proxied via SNI. An
|
||||
// empty slice means all domains are permitted.
|
||||
Allowlist []string
|
||||
|
||||
// DialContext is used to make the outgoing TCP connection.
|
||||
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// ReachableIPs enumerates the IP addresses this handler is reachable on.
|
||||
ReachableIPs []netip.Addr
|
||||
}
|
||||
|
||||
// ReachableOn returns the IP addresses this handler is reachable on.
|
||||
func (h *tcpSNIHandler) ReachableOn() []netip.Addr {
|
||||
return h.ReachableIPs
|
||||
}
|
||||
|
||||
func (h *tcpSNIHandler) Handle(c net.Conn) {
|
||||
addrPortStr := c.LocalAddr().String()
|
||||
_, port, err := net.SplitHostPort(addrPortStr)
|
||||
if err != nil {
|
||||
log.Printf("tcpSNIHandler.Handle: bogus addrPort %q", addrPortStr)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var p tcpproxy.Proxy
|
||||
p.ListenFunc = func(net, laddr string) (net.Listener, error) {
|
||||
return netutil.NewOneConnListener(c, nil), nil
|
||||
}
|
||||
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
|
||||
if len(h.Allowlist) > 0 {
|
||||
// TODO(tom): handle subdomains
|
||||
if slices.Index(h.Allowlist, sniName) < 0 {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return &tcpproxy.DialProxy{
|
||||
Addr: net.JoinHostPort(sniName, port),
|
||||
DialContext: h.DialContext,
|
||||
}, true
|
||||
})
|
||||
p.Start()
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/memnet"
|
||||
)
|
||||
|
||||
func echoConnOnce(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
b := make([]byte, 256)
|
||||
n, err := conn.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := conn.Write(b[:n]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPRoundRobinHandler(t *testing.T) {
|
||||
h := tcpRoundRobinHandler{
|
||||
To: []string{"yeet.com"},
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if network != "tcp" {
|
||||
t.Errorf("network = %s, want %s", network, "tcp")
|
||||
}
|
||||
if addr != "yeet.com:22" {
|
||||
t.Errorf("addr = %s, want %s", addr, "yeet.com:22")
|
||||
}
|
||||
|
||||
c, s := memnet.NewConn("outbound", 1024)
|
||||
go echoConnOnce(s)
|
||||
return c, nil
|
||||
},
|
||||
}
|
||||
|
||||
cSock, sSock := memnet.NewTCPConn(netip.MustParseAddrPort("10.64.1.2:22"), netip.MustParseAddrPort("10.64.1.2:22"), 1024)
|
||||
h.Handle(sSock)
|
||||
|
||||
// Test data write and read, the other end will echo back
|
||||
// a single stanza
|
||||
want := "hello"
|
||||
if _, err := io.WriteString(cSock, want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(want))
|
||||
if _, err := io.ReadAtLeast(cSock, got, len(got)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// The other end closed the socket after the first echo, so
|
||||
// any following read should error.
|
||||
io.WriteString(cSock, "deadass heres some data on god fr")
|
||||
if _, err := io.ReadAtLeast(cSock, got, len(got)); err == nil {
|
||||
t.Error("read succeeded on closed socket")
|
||||
}
|
||||
}
|
||||
|
||||
// Capture of first TCP data segment for a connection to https://pkgs.tailscale.com
|
||||
const tlsStart = `45000239ff1840004006f9f5c0a801f2
|
||||
c726b5efcf9e01bbe803b21394e3b752
|
||||
801801f641dc00000101080ade3474f2
|
||||
2fb93ee71603010200010001fc030303
|
||||
c3acbd19d2624765bb19af4bce03365e
|
||||
1d197f5bb939cdadeff26b0f8e7a0620
|
||||
295b04127b82bae46aac4ff58cffef25
|
||||
eba75a4b7a6de729532c411bd9dd0d2c
|
||||
00203a3a130113021303c02bc02fc02c
|
||||
c030cca9cca8c013c014009c009d002f
|
||||
003501000193caca0000000a000a0008
|
||||
1a1a001d001700180010000e000c0268
|
||||
3208687474702f312e31002b0007062a
|
||||
2a03040303ff01000100000d00120010
|
||||
04030804040105030805050108060601
|
||||
000b00020100002300000033002b0029
|
||||
1a1a000100001d0020d3c76bef062979
|
||||
a812ce935cfb4dbe6b3a84dc5ba9226f
|
||||
23b0f34af9d1d03b4a001b0003020002
|
||||
00120000446900050003026832000000
|
||||
170015000012706b67732e7461696c73
|
||||
63616c652e636f6d002d000201010005
|
||||
00050100000000001700003a3a000100
|
||||
0015002d000000000000000000000000
|
||||
00000000000000000000000000000000
|
||||
00000000000000000000000000000000
|
||||
0000290094006f0069e76f2016f963ad
|
||||
38c8632d1f240cd75e00e25fdef295d4
|
||||
7042b26f3a9a543b1c7dc74939d77803
|
||||
20527d423ff996997bda2c6383a14f49
|
||||
219eeef8a053e90a32228df37ddbe126
|
||||
eccf6b085c93890d08341d819aea6111
|
||||
0d909f4cd6b071d9ea40618e74588a33
|
||||
90d494bbb5c3002120d5a164a16c9724
|
||||
c9ef5e540d8d6f007789a7acf9f5f16f
|
||||
bf6a1907a6782ed02b`
|
||||
|
||||
func fakeSNIHeader() []byte {
|
||||
b, err := hex.DecodeString(strings.Replace(tlsStart, "\n", "", -1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b[0x34:] // trim IP + TCP header
|
||||
}
|
||||
|
||||
func TestTCPSNIHandler(t *testing.T) {
|
||||
h := tcpSNIHandler{
|
||||
Allowlist: []string{"pkgs.tailscale.com"},
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if network != "tcp" {
|
||||
t.Errorf("network = %s, want %s", network, "tcp")
|
||||
}
|
||||
if addr != "pkgs.tailscale.com:443" {
|
||||
t.Errorf("addr = %s, want %s", addr, "pkgs.tailscale.com:443")
|
||||
}
|
||||
|
||||
c, s := memnet.NewConn("outbound", 1024)
|
||||
go echoConnOnce(s)
|
||||
return c, nil
|
||||
},
|
||||
}
|
||||
|
||||
cSock, sSock := memnet.NewTCPConn(netip.MustParseAddrPort("10.64.1.2:22"), netip.MustParseAddrPort("10.64.1.2:443"), 1024)
|
||||
h.Handle(sSock)
|
||||
|
||||
// Fake a TLS handshake record with an SNI in it.
|
||||
if _, err := cSock.Write(fakeSNIHeader()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test read, the other end will echo back
|
||||
// a single stanza, which is at least the beginning of the SNI header.
|
||||
want := fakeSNIHeader()[:5]
|
||||
if _, err := cSock.Write(want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(want))
|
||||
if _, err := io.ReadAtLeast(cSock, got, len(got)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -40,12 +40,3 @@ type SetPushDeviceTokenRequest struct {
|
||||
// PushDeviceToken is the iOS/macOS APNs device token (and any future Android equivalent).
|
||||
PushDeviceToken string
|
||||
}
|
||||
|
||||
// ReloadConfigResponse is the response to a LocalAPI reload-config request.
|
||||
//
|
||||
// There are three possible outcomes: (false, "") if no config mode in use,
|
||||
// (true, "") on success, or (false, "error message") on failure.
|
||||
type ReloadConfigResponse struct {
|
||||
Reloaded bool // whether the config was reloaded
|
||||
Err string // any error message
|
||||
}
|
||||
|
||||
@@ -1244,25 +1244,6 @@ func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProf
|
||||
return current, all, err
|
||||
}
|
||||
|
||||
// ReloadConfig reloads the config file, if possible.
|
||||
func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/reload-config", 200, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res, err := decodeJSON[apitype.ReloadConfigResponse](body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if res.Err != "" {
|
||||
return false, errors.New(res.Err)
|
||||
}
|
||||
return res.Reloaded, nil
|
||||
}
|
||||
|
||||
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
|
||||
// profile is not assigned an ID until it is persisted after a successful login.
|
||||
// In order to login to the new profile, the user must call LoginInteractive.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react"
|
||||
import { Footer, Header, IP, State } from "src/components/legacy"
|
||||
import useAuth, { AuthResponse } from "src/hooks/auth"
|
||||
import useNodeData, { NodeData } from "src/hooks/node-data"
|
||||
import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
|
||||
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
|
||||
@@ -20,7 +19,14 @@ export default function App() {
|
||||
|
||||
return !needsLogin &&
|
||||
(data.DebugMode === "login" || data.DebugMode === "full") ? (
|
||||
<WebClient {...data} />
|
||||
<div className="flex flex-col items-center min-w-sm max-w-lg mx-auto py-10">
|
||||
{data.DebugMode === "login" ? (
|
||||
<LoginView {...data} />
|
||||
) : (
|
||||
<ManageView {...data} />
|
||||
)}
|
||||
<Footer className="mt-20" licensesURL={data.LicensesURL} />
|
||||
</div>
|
||||
) : (
|
||||
// Legacy client UI
|
||||
<div className="py-14">
|
||||
@@ -34,34 +40,7 @@ export default function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function WebClient(props: NodeData) {
|
||||
const { data: auth, loading: loadingAuth, waitOnAuth } = useAuth()
|
||||
|
||||
if (loadingAuth) {
|
||||
return <div className="text-center py-14">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center min-w-sm max-w-lg mx-auto py-10">
|
||||
{props.DebugMode === "full" && auth?.ok ? (
|
||||
<ManagementView {...props} />
|
||||
) : (
|
||||
<ReadonlyView data={props} auth={auth} waitOnAuth={waitOnAuth} />
|
||||
)}
|
||||
<Footer className="mt-20" licensesURL={props.LicensesURL} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReadonlyView({
|
||||
data,
|
||||
auth,
|
||||
waitOnAuth,
|
||||
}: {
|
||||
data: NodeData
|
||||
auth?: AuthResponse
|
||||
waitOnAuth: () => Promise<void>
|
||||
}) {
|
||||
function LoginView(props: NodeData) {
|
||||
return (
|
||||
<>
|
||||
<div className="pb-52 mx-auto">
|
||||
@@ -69,14 +48,14 @@ function ReadonlyView({
|
||||
</div>
|
||||
<div className="w-full p-4 bg-stone-50 rounded-3xl border border-gray-200 flex flex-col gap-4">
|
||||
<div className="flex gap-2.5">
|
||||
<ProfilePic url={data.Profile.ProfilePicURL} />
|
||||
<ProfilePic url={props.Profile.ProfilePicURL} />
|
||||
<div className="font-medium">
|
||||
<div className="text-neutral-500 text-xs uppercase tracking-wide">
|
||||
Owned by
|
||||
</div>
|
||||
<div className="text-neutral-800 text-sm leading-tight">
|
||||
{/* TODO(sonia): support tagged node profile view more eloquently */}
|
||||
{data.Profile.LoginName}
|
||||
{props.Profile.LoginName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,29 +64,19 @@ function ReadonlyView({
|
||||
<ConnectedDeviceIcon />
|
||||
<div className="text-neutral-800">
|
||||
<div className="text-lg font-medium leading-[25.20px]">
|
||||
{data.DeviceName}
|
||||
{props.DeviceName}
|
||||
</div>
|
||||
<div className="text-sm leading-tight">{data.IP}</div>
|
||||
<div className="text-sm leading-tight">{props.IP}</div>
|
||||
</div>
|
||||
</div>
|
||||
{data.DebugMode === "full" && (
|
||||
<button
|
||||
className="button button-blue ml-6"
|
||||
onClick={() => {
|
||||
window.open(auth?.authUrl, "_blank")
|
||||
waitOnAuth()
|
||||
}}
|
||||
>
|
||||
Access
|
||||
</button>
|
||||
)}
|
||||
<button className="button button-blue ml-6">Access</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ManagementView(props: NodeData) {
|
||||
function ManageView(props: NodeData) {
|
||||
return (
|
||||
<div className="px-5">
|
||||
<div className="flex justify-between mb-12">
|
||||
@@ -132,7 +101,6 @@ function ManagementView(props: NodeData) {
|
||||
Tailscale is up and running. You can connect to this device from devices
|
||||
in your tailnet by using its name or IP address.
|
||||
</p>
|
||||
<button className="button button-blue mt-6">Advertise exit node</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { apiFetch } from "src/api"
|
||||
|
||||
export type AuthResponse = {
|
||||
ok: boolean
|
||||
authUrl?: string
|
||||
}
|
||||
|
||||
// useAuth reports and refreshes Tailscale auth status
|
||||
// for the web client.
|
||||
export default function useAuth() {
|
||||
const [data, setData] = useState<AuthResponse>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const loadAuth = useCallback((wait?: boolean) => {
|
||||
const url = wait ? "/auth?wait=true" : "/auth"
|
||||
setLoading(true)
|
||||
return apiFetch(url, "GET")
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setLoading(false)
|
||||
setData(d)
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoading(false)
|
||||
console.error(error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadAuth()
|
||||
}, [])
|
||||
|
||||
const waitOnAuth = useCallback(() => loadAuth(true), [])
|
||||
|
||||
return { data, loading, waitOnAuth }
|
||||
}
|
||||
@@ -5,10 +5,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -38,8 +36,7 @@ import (
|
||||
|
||||
// Server is the backend server for a Tailscale web client.
|
||||
type Server struct {
|
||||
lc *tailscale.LocalClient
|
||||
timeNow func() time.Time
|
||||
lc *tailscale.LocalClient
|
||||
|
||||
devMode bool
|
||||
tsDebugMode string
|
||||
@@ -69,60 +66,53 @@ const (
|
||||
sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
|
||||
)
|
||||
|
||||
var (
|
||||
exitNodeRouteV4 = netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 = netip.MustParsePrefix("::/0")
|
||||
)
|
||||
|
||||
// browserSession holds data about a user's browser session
|
||||
// on the full management web client.
|
||||
type browserSession struct {
|
||||
// ID is the unique identifier for the session.
|
||||
// It is passed in the user's "TS-Web-Session" browser cookie.
|
||||
ID string
|
||||
SrcNode tailcfg.NodeID
|
||||
SrcNode tailcfg.StableNodeID
|
||||
SrcUser tailcfg.UserID
|
||||
AuthID string // from tailcfg.WebClientAuthResponse
|
||||
AuthURL string // from tailcfg.WebClientAuthResponse
|
||||
Created time.Time
|
||||
Authenticated bool
|
||||
AuthURL string // control server URL for user to authenticate the session
|
||||
Authenticated time.Time // when zero, authentication not complete
|
||||
}
|
||||
|
||||
// isAuthorized reports true if the given session is authorized
|
||||
// to be used by its associated user to access the full management
|
||||
// web client.
|
||||
//
|
||||
// isAuthorized is true only when s.Authenticated is true (i.e.
|
||||
// the user has authenticated the session) and the session is not
|
||||
// expired.
|
||||
// 2023-10-05: Sessions expire by default 30 days after creation.
|
||||
func (s *browserSession) isAuthorized(now time.Time) bool {
|
||||
// isAuthorized is true only when s.Authenticated is non-zero
|
||||
// (i.e. the user has authenticated the session) and the session
|
||||
// is not expired.
|
||||
// 2023-10-05: Sessions expire by default after 30 days.
|
||||
func (s *browserSession) isAuthorized() bool {
|
||||
switch {
|
||||
case s == nil:
|
||||
return false
|
||||
case !s.Authenticated:
|
||||
case s.Authenticated.IsZero():
|
||||
return false // awaiting auth
|
||||
case s.isExpired(now):
|
||||
case s.isExpired(): // TODO: add time field to server?
|
||||
return false // expired
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isExpired reports true if s is expired.
|
||||
// 2023-10-05: Sessions expire by default 30 days after creation.
|
||||
func (s *browserSession) isExpired(now time.Time) bool {
|
||||
return !s.Created.IsZero() && now.After(s.expires())
|
||||
}
|
||||
|
||||
// expires reports when the given session expires.
|
||||
func (s *browserSession) expires() time.Time {
|
||||
return s.Created.Add(sessionCookieExpiry)
|
||||
// 2023-10-05: Sessions expire by default after 30 days.
|
||||
// If s.Authenticated is zero, isExpired reports false.
|
||||
func (s *browserSession) isExpired() bool {
|
||||
return !s.Authenticated.IsZero() && s.Authenticated.Before(time.Now().Add(-sessionCookieExpiry)) // TODO: add time field to server?
|
||||
}
|
||||
|
||||
// ServerOpts contains options for constructing a new Server.
|
||||
type ServerOpts struct {
|
||||
DevMode bool
|
||||
|
||||
// LoginOnly indicates that the server should only serve the minimal
|
||||
// login client and not the full web client.
|
||||
LoginOnly bool
|
||||
|
||||
// CGIMode indicates if the server is running as a CGI script.
|
||||
CGIMode bool
|
||||
|
||||
@@ -132,26 +122,18 @@ type ServerOpts struct {
|
||||
// LocalClient is the tailscale.LocalClient to use for this web server.
|
||||
// If nil, a new one will be created.
|
||||
LocalClient *tailscale.LocalClient
|
||||
|
||||
// TimeNow optionally provides a time function.
|
||||
// time.Now is used as default.
|
||||
TimeNow func() time.Time
|
||||
}
|
||||
|
||||
// NewServer constructs a new Tailscale web client server.
|
||||
func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
|
||||
// The provided context should live for the duration of the Server's lifetime.
|
||||
func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) {
|
||||
if opts.LocalClient == nil {
|
||||
opts.LocalClient = &tailscale.LocalClient{}
|
||||
}
|
||||
s = &Server{
|
||||
devMode: opts.DevMode,
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
pathPrefix: opts.PathPrefix,
|
||||
timeNow: opts.TimeNow,
|
||||
}
|
||||
if s.timeNow == nil {
|
||||
s.timeNow = time.Now
|
||||
}
|
||||
s.tsDebugMode = s.debugMode()
|
||||
s.assetsHandler, cleanup = assetsHandler(opts.DevMode)
|
||||
@@ -202,75 +184,42 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
|
||||
if ok := s.authorizeRequest(w, r); !ok {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
// Pass API requests through to the API handler.
|
||||
s.apiHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if !s.devMode {
|
||||
s.lc.IncrementCounter(r.Context(), "web_client_page_load", 1)
|
||||
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
|
||||
}
|
||||
s.assetsHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// authorizeRequest reports whether the request from the web client
|
||||
// is authorized to be completed.
|
||||
// authorizePlatformRequest reports whether the request from the web client
|
||||
// is authorized to access the client for those platforms that support it.
|
||||
// It reports true if the request is authorized, and false otherwise.
|
||||
// authorizeRequest manages writing out any relevant authorization
|
||||
// authorizePlatformRequest manages writing out any relevant authorization
|
||||
// errors to the ResponseWriter itself.
|
||||
func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
if s.tsDebugMode == "full" { // client using tailscale auth
|
||||
_, err := s.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
switch {
|
||||
case err != nil:
|
||||
// All requests must be made over tailscale.
|
||||
http.Error(w, "must access over tailscale", http.StatusUnauthorized)
|
||||
return false
|
||||
case r.URL.Path == "/api/data" && r.Method == httpm.GET:
|
||||
// Readonly endpoint allowed without browser session.
|
||||
return true
|
||||
case r.URL.Path == "/api/auth":
|
||||
// Endpoint for browser to request auth allowed without browser session.
|
||||
return true
|
||||
case strings.HasPrefix(r.URL.Path, "/api/"):
|
||||
// All other /api/ endpoints require a valid browser session.
|
||||
//
|
||||
// TODO(sonia): s.getTailscaleBrowserSession calls whois again,
|
||||
// should try and use the above call instead of running another
|
||||
// localapi request.
|
||||
session, _, err := s.getTailscaleBrowserSession(r)
|
||||
if err != nil || !session.isAuthorized(s.timeNow()) {
|
||||
http.Error(w, "no valid session", http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
// No additional auth on non-api (assets, index.html, etc).
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Client using system-specific auth.
|
||||
d := distro.Get()
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/assets/") && r.Method == httpm.GET:
|
||||
// Don't require authorization for static assets.
|
||||
return true
|
||||
case d == distro.Synology:
|
||||
func authorizePlatformRequest(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
return authorizeSynology(w, r)
|
||||
case d == distro.QNAP:
|
||||
case distro.QNAP:
|
||||
return authorizeQNAP(w, r)
|
||||
default:
|
||||
return true // no additional auth for this distro
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// serveLoginAPI serves requests for the web login client.
|
||||
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
||||
// which protects the handler using gorilla csrf.
|
||||
func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
// The login client is run directly from client plugins,
|
||||
// so first authenticate and authorize the request for the host platform.
|
||||
if ok := authorizePlatformRequest(w, r); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
if r.URL.Path != "/api/data" { // only endpoint allowed for login client
|
||||
http.Error(w, "invalid endpoint", http.StatusNotFound)
|
||||
@@ -289,11 +238,10 @@ func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var (
|
||||
errNoSession = errors.New("no-browser-session")
|
||||
errNotUsingTailscale = errors.New("not-using-tailscale")
|
||||
errTaggedRemoteSource = errors.New("tagged-remote-source")
|
||||
errTaggedLocalSource = errors.New("tagged-local-source")
|
||||
errNotOwner = errors.New("not-owner")
|
||||
errNoSession = errors.New("no-browser-session")
|
||||
errNotUsingTailscale = errors.New("not-using-tailscale")
|
||||
errTaggedSource = errors.New("tagged-source")
|
||||
errNotOwner = errors.New("not-owner")
|
||||
)
|
||||
|
||||
// getTailscaleBrowserSession retrieves the browser session associated with
|
||||
@@ -305,13 +253,8 @@ var (
|
||||
//
|
||||
// - (errNoSession) The request does not have a session.
|
||||
//
|
||||
// - (errTaggedRemoteSource) The source is remote (another node) and tagged.
|
||||
// Users must use their own user-owned devices to manage other nodes'
|
||||
// web clients.
|
||||
//
|
||||
// - (errTaggedLocalSource) The source is local (the same node) and tagged.
|
||||
// Tagged nodes can only be remotely managed, allowing ACLs to dictate
|
||||
// access to web clients.
|
||||
// - (errTaggedSource) The source is a tagged node. Users must use their
|
||||
// own user-owned devices to manage other nodes' web clients.
|
||||
//
|
||||
// - (errNotOwner) The source is not the owner of this client (if the
|
||||
// client is user-owned). Only the owner is allowed to manage the
|
||||
@@ -320,122 +263,70 @@ var (
|
||||
// If no error is returned, the browserSession is always non-nil.
|
||||
// getTailscaleBrowserSession does not check whether the session has been
|
||||
// authorized by the user. Callers can use browserSession.isAuthorized.
|
||||
//
|
||||
// The WhoIsResponse is always populated, with a non-nil Node and UserProfile,
|
||||
// unless getTailscaleBrowserSession reports errNotUsingTailscale.
|
||||
func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, error) {
|
||||
whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
status, statusErr := s.lc.StatusWithoutPeers(r.Context())
|
||||
func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, error) {
|
||||
whoIs, err := s.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
switch {
|
||||
case whoIsErr != nil:
|
||||
return nil, nil, errNotUsingTailscale
|
||||
case statusErr != nil:
|
||||
return nil, whoIs, statusErr
|
||||
case status.Self == nil:
|
||||
return nil, whoIs, errors.New("missing self node in tailscale status")
|
||||
case whoIs.Node.IsTagged() && whoIs.Node.StableID == status.Self.ID:
|
||||
return nil, whoIs, errTaggedLocalSource
|
||||
case err != nil:
|
||||
return nil, errNotUsingTailscale
|
||||
case whoIs.Node.IsTagged():
|
||||
return nil, whoIs, errTaggedRemoteSource
|
||||
case !status.Self.IsTagged() && status.Self.UserID != whoIs.UserProfile.ID:
|
||||
return nil, whoIs, errNotOwner
|
||||
return nil, errTaggedSource
|
||||
}
|
||||
srcNode := whoIs.Node.ID
|
||||
srcNode := whoIs.Node.StableID
|
||||
srcUser := whoIs.UserProfile.ID
|
||||
|
||||
status, err := s.lc.StatusWithoutPeers(r.Context())
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case status.Self == nil:
|
||||
return nil, errors.New("missing self node in tailscale status")
|
||||
case !status.Self.IsTagged() && status.Self.UserID != srcUser:
|
||||
return nil, errNotOwner
|
||||
}
|
||||
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
return nil, whoIs, errNoSession
|
||||
return nil, errNoSession
|
||||
} else if err != nil {
|
||||
return nil, whoIs, err
|
||||
return nil, err
|
||||
}
|
||||
v, ok := s.browserSessions.Load(cookie.Value)
|
||||
if !ok {
|
||||
return nil, whoIs, errNoSession
|
||||
return nil, errNoSession
|
||||
}
|
||||
session := v.(*browserSession)
|
||||
if session.SrcNode != srcNode || session.SrcUser != srcUser {
|
||||
// In this case the browser cookie is associated with another tailscale node.
|
||||
// Maybe the source browser's machine was logged out and then back in as a different node.
|
||||
// Return errNoSession because there is no session for this user.
|
||||
return nil, whoIs, errNoSession
|
||||
} else if session.isExpired(s.timeNow()) {
|
||||
return nil, errNoSession
|
||||
} else if session.isExpired() {
|
||||
// Session expired, remove from session map and return errNoSession.
|
||||
s.browserSessions.Delete(session.ID)
|
||||
return nil, whoIs, errNoSession
|
||||
return nil, errNoSession
|
||||
}
|
||||
return session, whoIs, nil
|
||||
return session, nil
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
OK bool `json:"ok"` // true when user has valid auth session
|
||||
AuthURL string `json:"authUrl,omitempty"` // filled when user has control auth action to take
|
||||
Error string `json:"error,omitempty"` // filled when Ok is false
|
||||
}
|
||||
|
||||
func (s *Server) serveTailscaleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var resp authResponse
|
||||
|
||||
session, whois, err := s.getTailscaleBrowserSession(r)
|
||||
session, err := s.getTailscaleBrowserSession(r)
|
||||
switch {
|
||||
case err != nil && !errors.Is(err, errNoSession):
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
resp = authResponse{OK: false, Error: err.Error()}
|
||||
case session == nil:
|
||||
// Create a new session.
|
||||
d, err := s.getOrAwaitAuth(r.Context(), "", whois.Node.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sid, err := s.newSessionID()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
session := &browserSession{
|
||||
ID: sid,
|
||||
SrcNode: whois.Node.ID,
|
||||
SrcUser: whois.UserProfile.ID,
|
||||
AuthID: d.ID,
|
||||
AuthURL: d.URL,
|
||||
Created: s.timeNow(),
|
||||
}
|
||||
s.browserSessions.Store(sid, session)
|
||||
// Set the cookie on browser.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: sid,
|
||||
Raw: sid,
|
||||
Path: "/",
|
||||
Expires: session.expires(),
|
||||
})
|
||||
resp = authResponse{OK: false, AuthURL: d.URL}
|
||||
case !session.isAuthorized(s.timeNow()):
|
||||
if r.URL.Query().Get("wait") == "true" {
|
||||
// Client requested we block until user completes auth.
|
||||
d, err := s.getOrAwaitAuth(r.Context(), session.AuthID, whois.Node.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
// Clean up the session. Doing this on any error from control
|
||||
// server to avoid the user getting stuck with a bad session
|
||||
// cookie.
|
||||
s.browserSessions.Delete(session.ID)
|
||||
return
|
||||
}
|
||||
if d.Complete {
|
||||
session.Authenticated = d.Complete
|
||||
s.browserSessions.Store(session.ID, session)
|
||||
}
|
||||
}
|
||||
if session.isAuthorized(s.timeNow()) {
|
||||
resp = authResponse{OK: true}
|
||||
} else {
|
||||
resp = authResponse{OK: false, AuthURL: session.AuthURL}
|
||||
}
|
||||
// TODO(tailscale/corp#14335): Create a new auth path from control,
|
||||
// and store back to s.browserSessions and request cookie.
|
||||
case !session.isAuthorized():
|
||||
// TODO(tailscale/corp#14335): Check on the session auth path status from control,
|
||||
// and store back to s.browserSessions.
|
||||
default:
|
||||
resp = authResponse{OK: true}
|
||||
}
|
||||
@@ -447,71 +338,35 @@ func (s *Server) serveTailscaleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
func (s *Server) newSessionID() (string, error) {
|
||||
raw := make([]byte, 16)
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
cookie := "ts-web-" + base64.RawURLEncoding.EncodeToString(raw)
|
||||
if _, ok := s.browserSessions.Load(cookie); !ok {
|
||||
return cookie, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("too many collisions generating new session; please refresh page")
|
||||
}
|
||||
|
||||
// getOrAwaitAuth connects to the control server for user auth,
|
||||
// with the following behavior:
|
||||
//
|
||||
// 1. If authID is provided empty, a new auth URL is created on the control
|
||||
// server and reported back here, which can then be used to redirect the
|
||||
// user on the frontend.
|
||||
// 2. If authID is provided non-empty, the connection to control blocks until
|
||||
// the user has completed authenticating the associated auth URL,
|
||||
// or until ctx is canceled.
|
||||
func (s *Server) getOrAwaitAuth(ctx context.Context, authID string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error) {
|
||||
type data struct {
|
||||
ID string
|
||||
Src tailcfg.NodeID
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(data{ID: authID, Src: src}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := "http://" + apitype.LocalAPIHost + "/localapi/v0/debug-web-client"
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, &b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.lc.DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed request: %s", body)
|
||||
}
|
||||
var authResp *tailcfg.WebClientAuthResponse
|
||||
if err := json.Unmarshal(body, &authResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
// serveAPI serves requests for the web client api.
|
||||
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
||||
// which protects the handler using gorilla csrf.
|
||||
func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api")
|
||||
switch {
|
||||
case path == "/auth":
|
||||
if s.tsDebugMode == "full" { // behind debug flag
|
||||
if s.tsDebugMode == "full" {
|
||||
// tailscale/corp#14335: Only restrict to tailscale auth in debug "full" web client mode.
|
||||
// TODO(sonia,will): Switch serveAPI over to always require TS auth when we're ready
|
||||
// to remove the debug flags.
|
||||
// For now, existing client uses platform auth (else case below).
|
||||
|
||||
if r.URL.Path == "/api/auth" {
|
||||
// Serve auth, which creates a new session for the user to authenticate,
|
||||
// in the case that the request doesn't already have one.
|
||||
s.serveTailscaleAuth(w, r)
|
||||
return
|
||||
}
|
||||
// For all other endpoints, require a valid session to proceed.
|
||||
session, err := s.getTailscaleBrowserSession(r)
|
||||
if err != nil || !session.isAuthorized() {
|
||||
http.Error(w, "no valid session", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else if ok := authorizePlatformRequest(w, r); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api")
|
||||
switch {
|
||||
case path == "/data":
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
@@ -573,6 +428,8 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
IPNVersion: versionShort,
|
||||
DebugMode: s.tsDebugMode,
|
||||
}
|
||||
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
||||
for _, r := range prefs.AdvertiseRoutes {
|
||||
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
||||
data.AdvertiseExitNode = true
|
||||
@@ -617,22 +474,6 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
prefs, err := s.lc.GetPrefs(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
isCurrentlyExitNode := slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV4) || slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV6)
|
||||
|
||||
if postData.AdvertiseExitNode != isCurrentlyExitNode {
|
||||
if postData.AdvertiseExitNode {
|
||||
s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_enable", 1)
|
||||
} else {
|
||||
s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_disable", 1)
|
||||
}
|
||||
}
|
||||
|
||||
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"tailscale.com/net/memnet"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
func TestQnapAuthnURL(t *testing.T) {
|
||||
@@ -125,7 +124,7 @@ func TestServeAPI(t *testing.T) {
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
if gotStatus := res.StatusCode; tt.wantStatus != gotStatus {
|
||||
t.Errorf("wrong status; want=%v, got=%v", tt.wantStatus, gotStatus)
|
||||
t.Errorf("wrong status; want=%q, got=%q", tt.wantStatus, gotStatus)
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
@@ -151,50 +150,76 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
tags := views.SliceOf([]string{"tag:server"})
|
||||
tailnetNodes := map[string]*apitype.WhoIsResponse{
|
||||
userANodeIP: {
|
||||
Node: &tailcfg.Node{ID: 1, StableID: "1"},
|
||||
Node: &tailcfg.Node{StableID: "Node1"},
|
||||
UserProfile: userA,
|
||||
},
|
||||
userBNodeIP: {
|
||||
Node: &tailcfg.Node{ID: 2, StableID: "2"},
|
||||
Node: &tailcfg.Node{StableID: "Node2"},
|
||||
UserProfile: userB,
|
||||
},
|
||||
taggedNodeIP: {
|
||||
Node: &tailcfg.Node{ID: 3, StableID: "3", Tags: tags.AsSlice()},
|
||||
Node: &tailcfg.Node{StableID: "Node3", Tags: tags.AsSlice()},
|
||||
},
|
||||
}
|
||||
|
||||
lal := memnet.Listen("local-tailscaled.sock:80")
|
||||
defer lal.Close()
|
||||
localapi := mockLocalAPI(t, tailnetNodes, func() *ipnstate.PeerStatus { return selfNode })
|
||||
// Serve a testing localapi handler so we can simulate
|
||||
// whois responses without a functioning tailnet.
|
||||
localapi := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
addr := r.URL.Query().Get("addr")
|
||||
if addr == "" {
|
||||
t.Fatalf("/whois call missing \"addr\" query")
|
||||
}
|
||||
if node := tailnetNodes[addr]; node != nil {
|
||||
if err := json.NewEncoder(w).Encode(&node); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return
|
||||
}
|
||||
http.Error(w, "not a node", http.StatusUnauthorized)
|
||||
return
|
||||
case "/localapi/v0/status":
|
||||
status := ipnstate.Status{Self: selfNode}
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return
|
||||
default:
|
||||
// Only the above two endpoints get triggered from getTailscaleBrowserSession.
|
||||
// No need to mock any of the other localapi endpoint.
|
||||
t.Fatalf("unhandled localapi test endpoint %q, add to localapi handler func in test", r.URL.Path)
|
||||
}
|
||||
})}
|
||||
defer localapi.Close()
|
||||
go localapi.Serve(lal)
|
||||
|
||||
s := &Server{
|
||||
timeNow: time.Now,
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
}
|
||||
s := &Server{lc: &tailscale.LocalClient{Dial: lal.Dial}}
|
||||
|
||||
// Add some browser sessions to cache state.
|
||||
userASession := &browserSession{
|
||||
ID: "cookie1",
|
||||
SrcNode: 1,
|
||||
SrcNode: "Node1",
|
||||
SrcUser: userA.ID,
|
||||
Created: time.Now(),
|
||||
Authenticated: false, // not yet authenticated
|
||||
Authenticated: time.Time{}, // not yet authenticated
|
||||
}
|
||||
userBSession := &browserSession{
|
||||
ID: "cookie2",
|
||||
SrcNode: 2,
|
||||
SrcNode: "Node2",
|
||||
SrcUser: userB.ID,
|
||||
Created: time.Now().Add(-2 * sessionCookieExpiry),
|
||||
Authenticated: true, // expired
|
||||
Authenticated: time.Now().Add(-2 * sessionCookieExpiry), // expired
|
||||
}
|
||||
userASessionAuthorized := &browserSession{
|
||||
ID: "cookie3",
|
||||
SrcNode: 1,
|
||||
SrcNode: "Node1",
|
||||
SrcUser: userA.ID,
|
||||
Created: time.Now(),
|
||||
Authenticated: true, // authenticated and not expired
|
||||
Authenticated: time.Now(), // authenticated and not expired
|
||||
}
|
||||
s.browserSessions.Store(userASession.ID, userASession)
|
||||
s.browserSessions.Store(userBSession.ID, userBSession)
|
||||
@@ -240,26 +265,11 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
wantError: errNotOwner,
|
||||
},
|
||||
{
|
||||
name: "tagged-remote-source",
|
||||
name: "tagged-source",
|
||||
selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
|
||||
remoteAddr: taggedNodeIP,
|
||||
wantSession: nil,
|
||||
wantError: errTaggedRemoteSource,
|
||||
},
|
||||
{
|
||||
name: "tagged-local-source",
|
||||
selfNode: &ipnstate.PeerStatus{ID: "3"},
|
||||
remoteAddr: taggedNodeIP, // same node as selfNode
|
||||
wantSession: nil,
|
||||
wantError: errTaggedLocalSource,
|
||||
},
|
||||
{
|
||||
name: "not-tagged-local-source",
|
||||
selfNode: &ipnstate.PeerStatus{ID: "1", UserID: userA.ID},
|
||||
remoteAddr: userANodeIP, // same node as selfNode
|
||||
cookie: userASession.ID,
|
||||
wantSession: userASession,
|
||||
wantError: nil, // should not error
|
||||
wantError: errTaggedSource,
|
||||
},
|
||||
{
|
||||
name: "has-session",
|
||||
@@ -302,412 +312,16 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
if tt.cookie != "" {
|
||||
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tt.cookie})
|
||||
}
|
||||
session, _, err := s.getTailscaleBrowserSession(r)
|
||||
session, err := s.getTailscaleBrowserSession(r)
|
||||
if !errors.Is(err, tt.wantError) {
|
||||
t.Errorf("wrong error; want=%v, got=%v", tt.wantError, err)
|
||||
}
|
||||
if diff := cmp.Diff(session, tt.wantSession); diff != "" {
|
||||
t.Errorf("wrong session; (-got+want):%v", diff)
|
||||
}
|
||||
if gotIsAuthorized := session.isAuthorized(s.timeNow()); gotIsAuthorized != tt.wantIsAuthorized {
|
||||
if gotIsAuthorized := session.isAuthorized(); gotIsAuthorized != tt.wantIsAuthorized {
|
||||
t.Errorf("wrong isAuthorized; want=%v, got=%v", tt.wantIsAuthorized, gotIsAuthorized)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAuthorizeRequest tests the s.authorizeRequest function.
|
||||
// 2023-10-18: These tests currently cover tailscale auth mode (not platform auth).
|
||||
func TestAuthorizeRequest(t *testing.T) {
|
||||
// Create self and remoteNode owned by same user.
|
||||
// See TestGetTailscaleBrowserSession for tests of
|
||||
// browser sessions w/ different users.
|
||||
user := &tailcfg.UserProfile{ID: tailcfg.UserID(1)}
|
||||
self := &ipnstate.PeerStatus{ID: "self", UserID: user.ID}
|
||||
remoteNode := &apitype.WhoIsResponse{Node: &tailcfg.Node{StableID: "node"}, UserProfile: user}
|
||||
remoteIP := "100.100.100.101"
|
||||
|
||||
lal := memnet.Listen("local-tailscaled.sock:80")
|
||||
defer lal.Close()
|
||||
localapi := mockLocalAPI(t,
|
||||
map[string]*apitype.WhoIsResponse{remoteIP: remoteNode},
|
||||
func() *ipnstate.PeerStatus { return self },
|
||||
)
|
||||
defer localapi.Close()
|
||||
go localapi.Serve(lal)
|
||||
|
||||
s := &Server{
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
tsDebugMode: "full",
|
||||
timeNow: time.Now,
|
||||
}
|
||||
validCookie := "ts-cookie"
|
||||
s.browserSessions.Store(validCookie, &browserSession{
|
||||
ID: validCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: time.Now(),
|
||||
Authenticated: true,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
reqPath string
|
||||
reqMethod string
|
||||
|
||||
wantOkNotOverTailscale bool // simulates req over public internet
|
||||
wantOkWithoutSession bool // simulates req over TS without valid browser session
|
||||
wantOkWithSession bool // simulates req over TS with valid browser session
|
||||
}{{
|
||||
reqPath: "/api/data",
|
||||
reqMethod: httpm.GET,
|
||||
wantOkNotOverTailscale: false,
|
||||
wantOkWithoutSession: true,
|
||||
wantOkWithSession: true,
|
||||
}, {
|
||||
reqPath: "/api/data",
|
||||
reqMethod: httpm.POST,
|
||||
wantOkNotOverTailscale: false,
|
||||
wantOkWithoutSession: false,
|
||||
wantOkWithSession: true,
|
||||
}, {
|
||||
reqPath: "/api/auth",
|
||||
reqMethod: httpm.GET,
|
||||
wantOkNotOverTailscale: false,
|
||||
wantOkWithoutSession: true,
|
||||
wantOkWithSession: true,
|
||||
}, {
|
||||
reqPath: "/api/somethingelse",
|
||||
reqMethod: httpm.GET,
|
||||
wantOkNotOverTailscale: false,
|
||||
wantOkWithoutSession: false,
|
||||
wantOkWithSession: true,
|
||||
}, {
|
||||
reqPath: "/assets/styles.css",
|
||||
wantOkNotOverTailscale: false,
|
||||
wantOkWithoutSession: true,
|
||||
wantOkWithSession: true,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s-%s", tt.reqMethod, tt.reqPath), func(t *testing.T) {
|
||||
doAuthorize := func(remoteAddr string, cookie string) bool {
|
||||
r := httptest.NewRequest(tt.reqMethod, tt.reqPath, nil)
|
||||
r.RemoteAddr = remoteAddr
|
||||
if cookie != "" {
|
||||
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: cookie})
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
return s.authorizeRequest(w, r)
|
||||
}
|
||||
// Do request from non-Tailscale IP.
|
||||
if gotOk := doAuthorize("123.456.789.999", ""); gotOk != tt.wantOkNotOverTailscale {
|
||||
t.Errorf("wantOkNotOverTailscale; want=%v, got=%v", tt.wantOkNotOverTailscale, gotOk)
|
||||
}
|
||||
// Do request from Tailscale IP w/o associated session.
|
||||
if gotOk := doAuthorize(remoteIP, ""); gotOk != tt.wantOkWithoutSession {
|
||||
t.Errorf("wantOkWithoutSession; want=%v, got=%v", tt.wantOkWithoutSession, gotOk)
|
||||
}
|
||||
// Do request from Tailscale IP w/ associated session.
|
||||
if gotOk := doAuthorize(remoteIP, validCookie); gotOk != tt.wantOkWithSession {
|
||||
t.Errorf("wantOkWithSession; want=%v, got=%v", tt.wantOkWithSession, gotOk)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeTailscaleAuth(t *testing.T) {
|
||||
user := &tailcfg.UserProfile{ID: tailcfg.UserID(1)}
|
||||
self := &ipnstate.PeerStatus{ID: "self", UserID: user.ID}
|
||||
remoteNode := &apitype.WhoIsResponse{Node: &tailcfg.Node{ID: 1}, UserProfile: user}
|
||||
remoteIP := "100.100.100.101"
|
||||
|
||||
lal := memnet.Listen("local-tailscaled.sock:80")
|
||||
defer lal.Close()
|
||||
localapi := mockLocalAPI(t,
|
||||
map[string]*apitype.WhoIsResponse{remoteIP: remoteNode},
|
||||
func() *ipnstate.PeerStatus { return self },
|
||||
)
|
||||
defer localapi.Close()
|
||||
go localapi.Serve(lal)
|
||||
|
||||
timeNow := time.Now()
|
||||
oneHourAgo := timeNow.Add(-time.Hour)
|
||||
sixtyDaysAgo := timeNow.Add(-sessionCookieExpiry * 2)
|
||||
|
||||
s := &Server{
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
tsDebugMode: "full",
|
||||
timeNow: func() time.Time { return timeNow },
|
||||
}
|
||||
|
||||
successCookie := "ts-cookie-success"
|
||||
s.browserSessions.Store(successCookie, &browserSession{
|
||||
ID: successCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: oneHourAgo,
|
||||
AuthID: testAuthPathSuccess,
|
||||
AuthURL: testControlURL + testAuthPathSuccess,
|
||||
})
|
||||
failureCookie := "ts-cookie-failure"
|
||||
s.browserSessions.Store(failureCookie, &browserSession{
|
||||
ID: failureCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: oneHourAgo,
|
||||
AuthID: testAuthPathError,
|
||||
AuthURL: testControlURL + testAuthPathError,
|
||||
})
|
||||
expiredCookie := "ts-cookie-expired"
|
||||
s.browserSessions.Store(expiredCookie, &browserSession{
|
||||
ID: expiredCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: sixtyDaysAgo,
|
||||
AuthID: "/a/old-auth-url",
|
||||
AuthURL: testControlURL + "/a/old-auth-url",
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cookie string
|
||||
query string
|
||||
wantStatus int
|
||||
wantResp *authResponse
|
||||
wantNewCookie bool // new cookie generated
|
||||
wantSession *browserSession // session associated w/ cookie at end of request
|
||||
}{
|
||||
{
|
||||
name: "new-session-created",
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPath},
|
||||
wantNewCookie: true,
|
||||
wantSession: &browserSession{
|
||||
ID: "GENERATED_ID", // gets swapped for newly created ID by test
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: timeNow,
|
||||
AuthID: testAuthPath,
|
||||
AuthURL: testControlURL + testAuthPath,
|
||||
Authenticated: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "query-existing-incomplete-session",
|
||||
cookie: successCookie,
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPathSuccess},
|
||||
wantSession: &browserSession{
|
||||
ID: successCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: oneHourAgo,
|
||||
AuthID: testAuthPathSuccess,
|
||||
AuthURL: testControlURL + testAuthPathSuccess,
|
||||
Authenticated: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "transition-to-successful-session",
|
||||
cookie: successCookie,
|
||||
// query "wait" indicates the FE wants to make
|
||||
// local api call to wait until session completed.
|
||||
query: "wait=true",
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: &authResponse{OK: true},
|
||||
wantSession: &browserSession{
|
||||
ID: successCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: oneHourAgo,
|
||||
AuthID: testAuthPathSuccess,
|
||||
AuthURL: testControlURL + testAuthPathSuccess,
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "query-existing-complete-session",
|
||||
cookie: successCookie,
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: &authResponse{OK: true},
|
||||
wantSession: &browserSession{
|
||||
ID: successCookie,
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: oneHourAgo,
|
||||
AuthID: testAuthPathSuccess,
|
||||
AuthURL: testControlURL + testAuthPathSuccess,
|
||||
Authenticated: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "transition-to-failed-session",
|
||||
cookie: failureCookie,
|
||||
query: "wait=true",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantResp: nil,
|
||||
wantSession: nil, // session deleted
|
||||
},
|
||||
{
|
||||
name: "failed-session-cleaned-up",
|
||||
cookie: failureCookie,
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPath},
|
||||
wantNewCookie: true,
|
||||
wantSession: &browserSession{
|
||||
ID: "GENERATED_ID",
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: timeNow,
|
||||
AuthID: testAuthPath,
|
||||
AuthURL: testControlURL + testAuthPath,
|
||||
Authenticated: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expired-cookie-gets-new-session",
|
||||
cookie: expiredCookie,
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: &authResponse{OK: false, AuthURL: testControlURL + testAuthPath},
|
||||
wantNewCookie: true,
|
||||
wantSession: &browserSession{
|
||||
ID: "GENERATED_ID",
|
||||
SrcNode: remoteNode.Node.ID,
|
||||
SrcUser: user.ID,
|
||||
Created: timeNow,
|
||||
AuthID: testAuthPath,
|
||||
AuthURL: testControlURL + testAuthPath,
|
||||
Authenticated: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "/api/auth", nil)
|
||||
r.URL.RawQuery = tt.query
|
||||
r.RemoteAddr = remoteIP
|
||||
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tt.cookie})
|
||||
w := httptest.NewRecorder()
|
||||
s.serveTailscaleAuth(w, r)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
// Validate response status/data.
|
||||
if gotStatus := res.StatusCode; tt.wantStatus != gotStatus {
|
||||
t.Errorf("wrong status; want=%v, got=%v", tt.wantStatus, gotStatus)
|
||||
}
|
||||
var gotResp *authResponse
|
||||
if res.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(body, &gotResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(gotResp, tt.wantResp); diff != "" {
|
||||
t.Errorf("wrong response; (-got+want):%v", diff)
|
||||
}
|
||||
// Validate cookie creation.
|
||||
sessionID := tt.cookie
|
||||
var gotCookie bool
|
||||
for _, c := range w.Result().Cookies() {
|
||||
if c.Name == sessionCookieName {
|
||||
gotCookie = true
|
||||
sessionID = c.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
if gotCookie != tt.wantNewCookie {
|
||||
t.Errorf("wantNewCookie wrong; want=%v, got=%v", tt.wantNewCookie, gotCookie)
|
||||
}
|
||||
// Validate browser session contents.
|
||||
var gotSesson *browserSession
|
||||
if s, ok := s.browserSessions.Load(sessionID); ok {
|
||||
gotSesson = s.(*browserSession)
|
||||
}
|
||||
if tt.wantSession != nil && tt.wantSession.ID == "GENERATED_ID" {
|
||||
// If requested, swap in the generated session ID before
|
||||
// comparing got/want.
|
||||
tt.wantSession.ID = sessionID
|
||||
}
|
||||
if diff := cmp.Diff(gotSesson, tt.wantSession); diff != "" {
|
||||
t.Errorf("wrong session; (-got+want):%v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
testControlURL = "http://localhost:8080"
|
||||
testAuthPath = "/a/12345"
|
||||
testAuthPathSuccess = "/a/will-succeed"
|
||||
testAuthPathError = "/a/will-error"
|
||||
)
|
||||
|
||||
// mockLocalAPI constructs a test localapi handler that can be used
|
||||
// to simulate localapi responses without a functioning tailnet.
|
||||
//
|
||||
// self accepts a function that resolves to a self node status,
|
||||
// so that tests may swap out the /localapi/v0/status response
|
||||
// as desired.
|
||||
func mockLocalAPI(t *testing.T, whoIs map[string]*apitype.WhoIsResponse, self func() *ipnstate.PeerStatus) *http.Server {
|
||||
return &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
addr := r.URL.Query().Get("addr")
|
||||
if addr == "" {
|
||||
t.Fatalf("/whois call missing \"addr\" query")
|
||||
}
|
||||
if node := whoIs[addr]; node != nil {
|
||||
if err := json.NewEncoder(w).Encode(&node); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return
|
||||
}
|
||||
http.Error(w, "not a node", http.StatusUnauthorized)
|
||||
return
|
||||
case "/localapi/v0/status":
|
||||
status := ipnstate.Status{Self: self()}
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return
|
||||
case "/localapi/v0/debug-web-client": // used by TestServeTailscaleAuth
|
||||
type reqData struct {
|
||||
ID string
|
||||
Src tailcfg.NodeID
|
||||
}
|
||||
var data reqData
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if data.Src == 0 {
|
||||
http.Error(w, "missing Src node", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var resp *tailcfg.WebClientAuthResponse
|
||||
if data.ID == "" {
|
||||
resp = &tailcfg.WebClientAuthResponse{ID: testAuthPath, URL: testControlURL + testAuthPath}
|
||||
} else if data.ID == testAuthPathSuccess {
|
||||
resp = &tailcfg.WebClientAuthResponse{Complete: true}
|
||||
} else if data.ID == testAuthPathError {
|
||||
http.Error(w, "authenticated as wrong user", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return
|
||||
default:
|
||||
t.Fatalf("unhandled localapi test endpoint %q, add to localapi handler func in test", r.URL.Path)
|
||||
}
|
||||
})}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"tailscale.com/clientupdate/distsign"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cmpver"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -73,12 +72,11 @@ type Arguments struct {
|
||||
//
|
||||
// Leaving this empty is the same as using CurrentTrack.
|
||||
Version string
|
||||
// AppStore forces a local app store check, even if the current binary was
|
||||
// not installed via an app store. TODO(cpalmer): Remove this.
|
||||
AppStore bool
|
||||
// Logf is a logger for update progress messages.
|
||||
Logf logger.Logf
|
||||
// Stdout and Stderr should be used for output instead of os.Stdout and
|
||||
// os.Stderr.
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
// Confirm is called when a new version is available and should return true
|
||||
// if this new version should be installed. When Confirm returns false, the
|
||||
// update is aborted.
|
||||
@@ -86,10 +84,6 @@ type Arguments struct {
|
||||
// PkgsAddr is the address of the pkgs server to fetch updates from.
|
||||
// Defaults to "https://pkgs.tailscale.com".
|
||||
PkgsAddr string
|
||||
// ForAutoUpdate should be true when Updater is created in auto-update
|
||||
// context. When true, NewUpdater returns an error if it cannot be used for
|
||||
// auto-updates (even if Updater.Update field is non-nil).
|
||||
ForAutoUpdate bool
|
||||
}
|
||||
|
||||
func (args Arguments) validate() error {
|
||||
@@ -114,20 +108,10 @@ func NewUpdater(args Arguments) (*Updater, error) {
|
||||
up := Updater{
|
||||
Arguments: args,
|
||||
}
|
||||
if up.Stdout == nil {
|
||||
up.Stdout = os.Stdout
|
||||
}
|
||||
if up.Stderr == nil {
|
||||
up.Stderr = os.Stderr
|
||||
}
|
||||
var canAutoUpdate bool
|
||||
up.Update, canAutoUpdate = up.getUpdateFunction()
|
||||
up.Update = up.getUpdateFunction()
|
||||
if up.Update == nil {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
if args.ForAutoUpdate && !canAutoUpdate {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
switch up.Version {
|
||||
case StableTrack, UnstableTrack:
|
||||
up.track = up.Version
|
||||
@@ -152,70 +136,52 @@ func NewUpdater(args Arguments) (*Updater, error) {
|
||||
|
||||
type updateFunction func() error
|
||||
|
||||
func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
||||
func (up *Updater) getUpdateFunction() updateFunction {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return up.updateWindows, true
|
||||
return up.updateWindows
|
||||
case "linux":
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
// Synology updates use our own pkgs.tailscale.com instead of the
|
||||
// Synology Package Center. We should eventually get to a regular
|
||||
// release cadence with Synology Package Center and use their
|
||||
// auto-update mechanism.
|
||||
return up.updateSynology, false
|
||||
return up.updateSynology
|
||||
case distro.Debian: // includes Ubuntu
|
||||
return up.updateDebLike, true
|
||||
return up.updateDebLike
|
||||
case distro.Arch:
|
||||
if up.archPackageInstalled() {
|
||||
// Arch update func just prints a message about how to update,
|
||||
// it doesn't support auto-updates.
|
||||
return up.updateArchLike, false
|
||||
}
|
||||
return up.updateLinuxBinary, true
|
||||
return up.updateArchLike
|
||||
case distro.Alpine:
|
||||
return up.updateAlpineLike, true
|
||||
return up.updateAlpineLike
|
||||
}
|
||||
switch {
|
||||
case haveExecutable("pacman"):
|
||||
if up.archPackageInstalled() {
|
||||
// Arch update func just prints a message about how to update,
|
||||
// it doesn't support auto-updates.
|
||||
return up.updateArchLike, false
|
||||
}
|
||||
return up.updateLinuxBinary, true
|
||||
return up.updateArchLike
|
||||
case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
|
||||
// The distro.Debian switch case above should catch most apt-based
|
||||
// systems, but add this fallback just in case.
|
||||
return up.updateDebLike, true
|
||||
return up.updateDebLike
|
||||
case haveExecutable("dnf"):
|
||||
return up.updateFedoraLike("dnf"), true
|
||||
return up.updateFedoraLike("dnf")
|
||||
case haveExecutable("yum"):
|
||||
return up.updateFedoraLike("yum"), true
|
||||
return up.updateFedoraLike("yum")
|
||||
case haveExecutable("apk"):
|
||||
return up.updateAlpineLike, true
|
||||
return up.updateAlpineLike
|
||||
}
|
||||
// If nothing matched, fall back to tarball updates.
|
||||
if up.Update == nil {
|
||||
return up.updateLinuxBinary, true
|
||||
return up.updateLinuxBinary
|
||||
}
|
||||
case "darwin":
|
||||
switch {
|
||||
case version.IsMacAppStore():
|
||||
// App store update func just opens the store page, it doesn't
|
||||
// support auto-updates.
|
||||
return up.updateMacAppStore, false
|
||||
case version.IsMacSysExt():
|
||||
// Macsys update func kicks off Sparkle. Auto-updates are done by
|
||||
// Sparkle.
|
||||
return up.updateMacSys, false
|
||||
case !up.Arguments.AppStore && !version.IsSandboxedMacOS():
|
||||
return nil
|
||||
case !up.Arguments.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
|
||||
return up.updateMacSys
|
||||
default:
|
||||
return nil, false
|
||||
return up.updateMacAppStore
|
||||
}
|
||||
case "freebsd":
|
||||
return up.updateFreeBSD, true
|
||||
return up.updateFreeBSD
|
||||
}
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update runs a single update attempt using the platform-specific mechanism.
|
||||
@@ -235,13 +201,9 @@ func Update(args Arguments) error {
|
||||
}
|
||||
|
||||
func (up *Updater) confirm(ver string) bool {
|
||||
switch cmpver.Compare(version.Short(), ver) {
|
||||
case 0:
|
||||
if version.Short() == ver {
|
||||
up.Logf("already running %v; no update needed", ver)
|
||||
return false
|
||||
case 1:
|
||||
up.Logf("installed version %v is newer than the latest available version %v; no update needed", version.Short(), ver)
|
||||
return false
|
||||
}
|
||||
if up.Confirm != nil {
|
||||
return up.Confirm(ver)
|
||||
@@ -272,12 +234,12 @@ func (up *Updater) updateSynology() error {
|
||||
return fmt.Errorf("cannot find Synology package for os=%s arch=%s, please report a bug with your device model", osName, arch)
|
||||
}
|
||||
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !up.confirm(latest.SPKsVersion) {
|
||||
return nil
|
||||
}
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Download the SPK into a temporary directory.
|
||||
spkDir, err := os.MkdirTemp("", "tailscale-update")
|
||||
@@ -294,9 +256,9 @@ func (up *Updater) updateSynology() error {
|
||||
// connected over tailscale ssh and this parent process dies. Otherwise, if
|
||||
// you abort synopkg install mid-way, tailscaled is not restarted.
|
||||
cmd := exec.Command("nohup", "synopkg", "install", spkPath)
|
||||
// Don't attach cmd.Stdout to Stdout because nohup will redirect that into
|
||||
// nohup.out file. synopkg doesn't have any progress output anyway, it just
|
||||
// spits out a JSON result when done.
|
||||
// Don't attach cmd.Stdout to os.Stdout because nohup will redirect that
|
||||
// into nohup.out file. synopkg doesn't have any progress output anyway, it
|
||||
// just spits out a JSON result when done.
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
|
||||
@@ -407,15 +369,15 @@ func (up *Updater) updateDebLike() error {
|
||||
// we're not updating them:
|
||||
"-o", "APT::Get::List-Cleanup=0",
|
||||
)
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -480,12 +442,12 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (up *Updater) archPackageInstalled() bool {
|
||||
err := exec.Command("pacman", "--query", "tailscale").Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (up *Updater) updateArchLike() error {
|
||||
if err := exec.Command("pacman", "--query", "tailscale").Run(); err != nil && isExitError(err) {
|
||||
// Tailscale was not installed via pacman, update via tarball download
|
||||
// instead.
|
||||
return up.updateLinuxBinary()
|
||||
}
|
||||
// Arch maintainer asked us not to implement "tailscale update" or
|
||||
// auto-updates on Arch-based distros:
|
||||
// https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106
|
||||
@@ -529,8 +491,8 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
|
||||
}
|
||||
|
||||
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -615,8 +577,8 @@ func (up *Updater) updateAlpineLike() (err error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("apk", "upgrade", "tailscale")
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using apk: %w", err)
|
||||
}
|
||||
@@ -646,64 +608,73 @@ func (up *Updater) updateMacSys() error {
|
||||
}
|
||||
|
||||
func (up *Updater) updateMacAppStore() error {
|
||||
// We can't trigger the update via App Store from the sandboxed app. At
|
||||
// most, we can open the App Store page for them.
|
||||
up.Logf("Please use the App Store to update Tailscale.\nConsider enabling Automatic Updates in the App Store Settings, if you haven't already.\nOpening the Tailscale app page...")
|
||||
|
||||
out, err := exec.Command("open", "https://apps.apple.com/us/app/tailscale/id1475387142").CombinedOutput()
|
||||
out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't open the Tailscale page in App Store: %w, output: %q", err, string(out))
|
||||
return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out))
|
||||
}
|
||||
const on = "1\n"
|
||||
if string(out) != on {
|
||||
up.Logf("NOTE: Automatic updating for App Store apps is turned off. You can change this setting in System Settings (search for ‘update’).")
|
||||
}
|
||||
|
||||
out, err = exec.Command("softwareupdate", "--list").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't check App Store for available updates: %w, output: %q", err, string(out))
|
||||
}
|
||||
|
||||
newTailscale := parseSoftwareupdateList(out)
|
||||
if newTailscale == "" {
|
||||
up.Logf("no Tailscale update available")
|
||||
return nil
|
||||
}
|
||||
|
||||
newTailscaleVer := strings.TrimPrefix(newTailscale, "Tailscale-")
|
||||
if !up.confirm(newTailscaleVer) {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// winMSIEnv is the environment variable that, if set, is the MSI file for
|
||||
// the update command to install. It's passed like this so we can stop the
|
||||
// tailscale.exe process from running before the msiexec process runs and
|
||||
// tries to overwrite ourselves.
|
||||
winMSIEnv = "TS_UPDATE_WIN_MSI"
|
||||
// winExePathEnv is the environment variable that is set along with
|
||||
// winMSIEnv and carries the full path of the calling tailscale.exe binary.
|
||||
// It is used to re-launch the GUI process (tailscale-ipn.exe) after
|
||||
// install is complete.
|
||||
winExePathEnv = "TS_UPDATE_WIN_EXE_PATH"
|
||||
)
|
||||
var macOSAppStoreListPattern = regexp.MustCompile(`(?m)^\s+\*\s+Label:\s*(Tailscale-\d[\d\.]+)`)
|
||||
|
||||
// parseSoftwareupdateList searches the output of `softwareupdate --list` on
|
||||
// Darwin and returns the matching Tailscale package label. If there is none,
|
||||
// returns the empty string.
|
||||
//
|
||||
// See TestParseSoftwareupdateList for example inputs.
|
||||
func parseSoftwareupdateList(stdout []byte) string {
|
||||
matches := macOSAppStoreListPattern.FindSubmatch(stdout)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
return string(matches[1])
|
||||
}
|
||||
|
||||
// winMSIEnv is the environment variable that, if set, is the MSI file for the
|
||||
// update command to install. It's passed like this so we can stop the
|
||||
// tailscale.exe process from running before the msiexec process runs and tries
|
||||
// to overwrite ourselves.
|
||||
const winMSIEnv = "TS_UPDATE_WIN_MSI"
|
||||
|
||||
var (
|
||||
verifyAuthenticode func(string) error // or nil on non-Windows
|
||||
markTempFileFunc func(string) error // or nil on non-Windows
|
||||
launchTailscaleAsWinGUIUser func(string) error // or nil on non-Windows
|
||||
verifyAuthenticode func(string) error // or nil on non-Windows
|
||||
markTempFileFunc func(string) error // or nil on non-Windows
|
||||
)
|
||||
|
||||
func (up *Updater) updateWindows() error {
|
||||
if msi := os.Getenv(winMSIEnv); msi != "" {
|
||||
// stdout/stderr from this part of the install could be lost since the
|
||||
// parent tailscaled is replaced. Create a temp log file to have some
|
||||
// output to debug with in case update fails.
|
||||
close, err := up.switchOutputToFile()
|
||||
if err != nil {
|
||||
up.Logf("failed to create log file for installation: %v; proceeding with existing outputs", err)
|
||||
} else {
|
||||
defer close.Close()
|
||||
}
|
||||
|
||||
up.Logf("installing %v ...", msi)
|
||||
if err := up.installMSI(msi); err != nil {
|
||||
up.Logf("MSI install failed: %v", err)
|
||||
return err
|
||||
}
|
||||
up.Logf("relaunching tailscale-ipn.exe...")
|
||||
exePath := os.Getenv(winExePathEnv)
|
||||
if exePath == "" {
|
||||
up.Logf("env var %q not passed to installer binary copy", winExePathEnv)
|
||||
return fmt.Errorf("env var %q not passed to installer binary copy", winExePathEnv)
|
||||
}
|
||||
if err := launchTailscaleAsWinGUIUser(exePath); err != nil {
|
||||
up.Logf("Failed to re-launch tailscale after update: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
up.Logf("success.")
|
||||
return nil
|
||||
}
|
||||
@@ -716,12 +687,12 @@ func (up *Updater) updateWindows() error {
|
||||
arch = "x86"
|
||||
}
|
||||
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
return errors.New("must be run as Administrator")
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
return errors.New("must be run as Administrator")
|
||||
}
|
||||
|
||||
tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
|
||||
msiDir := filepath.Join(tsDir, "MSICache")
|
||||
@@ -746,7 +717,7 @@ func (up *Updater) updateWindows() error {
|
||||
up.Logf("authenticode verification succeeded")
|
||||
|
||||
up.Logf("making tailscale.exe copy to switch to...")
|
||||
selfOrig, selfCopy, err := makeSelfCopy()
|
||||
selfCopy, err := makeSelfCopy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -754,9 +725,9 @@ func (up *Updater) updateWindows() error {
|
||||
up.Logf("running tailscale.exe copy for final install...")
|
||||
|
||||
cmd := exec.Command(selfCopy, "update")
|
||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig)
|
||||
cmd.Stdout = up.Stderr
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
@@ -767,44 +738,18 @@ func (up *Updater) updateWindows() error {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (up *Updater) switchOutputToFile() (io.Closer, error) {
|
||||
var logFilePath string
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
logFilePath = filepath.Join(os.TempDir(), "tailscale-updater.log")
|
||||
} else {
|
||||
logFilePath = strings.TrimSuffix(exePath, ".exe") + ".log"
|
||||
}
|
||||
|
||||
up.Logf("writing update output to %q", logFilePath)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
up.Logf = func(m string, args ...any) {
|
||||
fmt.Fprintf(logFile, m+"\n", args...)
|
||||
}
|
||||
up.Stdout = logFile
|
||||
up.Stderr = logFile
|
||||
return logFile, nil
|
||||
}
|
||||
|
||||
func (up *Updater) installMSI(msi string) error {
|
||||
var err error
|
||||
for tries := 0; tries < 2; tries++ {
|
||||
// TS_NOLAUNCH: don't automatically launch the app after install.
|
||||
// We will launch it explicitly as the current GUI user afterwards.
|
||||
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn", "TS_NOLAUNCH=true")
|
||||
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
|
||||
cmd.Dir = filepath.Dir(msi)
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
up.Logf("Install attempt failed: %v", err)
|
||||
uninstallVersion := version.Short()
|
||||
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
|
||||
uninstallVersion = v
|
||||
@@ -812,8 +757,8 @@ func (up *Updater) installMSI(msi string) error {
|
||||
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
|
||||
up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
|
||||
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
up.Logf("msiexec uninstall: %v", err)
|
||||
@@ -834,30 +779,30 @@ func msiUUIDForVersion(ver string) string {
|
||||
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
|
||||
}
|
||||
|
||||
func makeSelfCopy() (origPathExe, tmpPathExe string, err error) {
|
||||
func makeSelfCopy() (tmpPathExe string, err error) {
|
||||
selfExe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
f, err := os.Open(selfExe)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
if f := markTempFileFunc; f != nil {
|
||||
if err := f(f2.Name()); err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(f2, f); err != nil {
|
||||
f2.Close()
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
return selfExe, f2.Name(), f2.Close()
|
||||
return f2.Name(), f2.Close()
|
||||
}
|
||||
|
||||
func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
|
||||
@@ -901,8 +846,8 @@ func (up *Updater) updateFreeBSD() (err error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("pkg", "upgrade", "tailscale")
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using pkg: %w", err)
|
||||
}
|
||||
@@ -914,13 +859,13 @@ func (up *Updater) updateLinuxBinary() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
// Root is needed to overwrite binaries and restart systemd unit.
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
|
||||
dlPath, err := up.downloadLinuxTarball(ver)
|
||||
if err != nil {
|
||||
@@ -949,7 +894,7 @@ func (up *Updater) updateLinuxBinary() error {
|
||||
func (up *Updater) downloadLinuxTarball(ver string) (string, error) {
|
||||
dlDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
dlDir = os.TempDir()
|
||||
return "", err
|
||||
}
|
||||
dlDir = filepath.Join(dlDir, "tailscale-update")
|
||||
if err := os.MkdirAll(dlDir, 0700); err != nil {
|
||||
|
||||
@@ -84,6 +84,84 @@ func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSoftwareupdateList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "update-at-end-of-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: ProAppsQTCodecs-1.0
|
||||
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
||||
* Label: Tailscale-1.23.4
|
||||
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
|
||||
`),
|
||||
want: "Tailscale-1.23.4",
|
||||
},
|
||||
{
|
||||
name: "update-in-middle-of-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: Tailscale-1.23.5000
|
||||
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
|
||||
* Label: ProAppsQTCodecs-1.0
|
||||
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
||||
`),
|
||||
want: "Tailscale-1.23.5000",
|
||||
},
|
||||
{
|
||||
name: "update-not-in-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: ProAppsQTCodecs-1.0
|
||||
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
||||
`),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "decoy-in-list",
|
||||
input: []byte(`
|
||||
Software Update Tool
|
||||
|
||||
Finding available software
|
||||
Software Update found the following new or updated software:
|
||||
* Label: MacBookAirEFIUpdate2.4-2.4
|
||||
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
||||
* Label: Malware-1.0
|
||||
Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH,
|
||||
`),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := parseSoftwareupdateList(test.input)
|
||||
if test.want != got {
|
||||
t.Fatalf("got %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateYUMRepoTrack(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
|
||||
@@ -7,20 +7,13 @@
|
||||
package clientupdate
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/authenticode"
|
||||
)
|
||||
|
||||
func init() {
|
||||
markTempFileFunc = markTempFileWindows
|
||||
verifyAuthenticode = verifyTailscale
|
||||
launchTailscaleAsWinGUIUser = launchTailscaleAsGUIUser
|
||||
}
|
||||
|
||||
func markTempFileWindows(name string) error {
|
||||
@@ -33,25 +26,3 @@ const certSubjectTailscale = "Tailscale Inc."
|
||||
func verifyTailscale(path string) error {
|
||||
return authenticode.Verify(path, certSubjectTailscale)
|
||||
}
|
||||
|
||||
func launchTailscaleAsGUIUser(exePath string) error {
|
||||
exePath = filepath.Join(filepath.Dir(exePath), "tailscale-ipn.exe")
|
||||
|
||||
var token windows.Token
|
||||
if u, err := user.Current(); err == nil && u.Name == "SYSTEM" {
|
||||
sessionID := winutil.WTSGetActiveConsoleSessionId()
|
||||
if sessionID != 0xFFFFFFFF {
|
||||
if err := windows.WTSQueryUserToken(sessionID, &token); err != nil {
|
||||
return err
|
||||
}
|
||||
defer token.Close()
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(exePath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Token: syscall.Token(token),
|
||||
HideWindow: true,
|
||||
}
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
-----BEGIN ROOT PUBLIC KEY-----
|
||||
ZjjKhUHBtLNRSO1dhOTjrXJGJ8lDe1594WM2XDuheVQ=
|
||||
Muw5GkO5mASsJ7k6kS+svfuanr6XcW9I7fPGtyqOTeI=
|
||||
-----END ROOT PUBLIC KEY-----
|
||||
@@ -19,7 +19,8 @@
|
||||
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
|
||||
// destination.
|
||||
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
|
||||
// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
|
||||
// - TS_EXTRA_ARGS: extra arguments to 'tailscale login', these are not
|
||||
// reset on restart.
|
||||
// - TS_USERSPACE: run with userspace networking (the default)
|
||||
// instead of kernel networking.
|
||||
// - TS_STATE_DIR: the directory in which to store tailscaled
|
||||
@@ -35,9 +36,15 @@
|
||||
// - TS_SOCKET: the path where the tailscaled LocalAPI socket should
|
||||
// be created.
|
||||
// - TS_AUTH_ONCE: if true, only attempt to log in if not already
|
||||
// logged in. If false (the default, for backwards
|
||||
// compatibility), forcibly log in every time the
|
||||
// container starts.
|
||||
// logged in. If false, forcibly log in every time the container starts.
|
||||
// The default until 1.50.0 was false, but that was misleading: until
|
||||
// 1.50, containerboot used `tailscale up` which would ignore an authkey
|
||||
// argument if there was already a node key. Effectively, this behaved
|
||||
// as though TS_AUTH_ONCE were always true.
|
||||
// In 1.50.0 the change was made to use `tailscale login` instead of `up`,
|
||||
// and login will reauthenticate every time it is given an authkey.
|
||||
// In 1.50.1 we set the TS_AUTH_ONCE to true, to match the previously
|
||||
// observed behavior.
|
||||
// - TS_SERVE_CONFIG: if specified, is the file path where the ipn.ServeConfig is located.
|
||||
// It will be applied once tailscaled is up and running. If the file contains
|
||||
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
|
||||
@@ -77,19 +84,10 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/linuxfw"
|
||||
)
|
||||
|
||||
func newNetfilterRunner(logf logger.Logf) (linuxfw.NetfilterRunner, error) {
|
||||
if defaultBool("TS_TEST_FAKE_NETFILTER", false) {
|
||||
return linuxfw.NewFakeIPTablesRunner(), nil
|
||||
}
|
||||
return linuxfw.New(logf)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetPrefix("boot: ")
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
@@ -111,7 +109,7 @@ func main() {
|
||||
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
|
||||
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
|
||||
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", true),
|
||||
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
|
||||
}
|
||||
|
||||
@@ -205,7 +203,7 @@ func main() {
|
||||
}
|
||||
didLogin = true
|
||||
w.Close()
|
||||
if err := tailscaleUp(bootCtx, cfg); err != nil {
|
||||
if err := tailscaleLogin(bootCtx, cfg); err != nil {
|
||||
return fmt.Errorf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
w, err = client.WatchIPNBus(bootCtx, ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
|
||||
@@ -255,12 +253,10 @@ authLoop:
|
||||
ctx, cancel := context.WithCancel(context.Background()) // no deadline now that we're in steady state
|
||||
defer cancel()
|
||||
|
||||
if cfg.AuthOnce {
|
||||
// Now that we are authenticated, we can set/reset any of the
|
||||
// settings that we need to.
|
||||
if err := tailscaleSet(ctx, cfg); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
// Now that we are authenticated, we can set/reset any of the
|
||||
// settings that we need to.
|
||||
if err := tailscaleSet(ctx, cfg); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ServeConfigPath != "" {
|
||||
@@ -299,13 +295,6 @@ authLoop:
|
||||
if cfg.ServeConfigPath != "" {
|
||||
go watchServeConfigChanges(ctx, cfg.ServeConfigPath, certDomainChanged, certDomain, client)
|
||||
}
|
||||
var nfr linuxfw.NetfilterRunner
|
||||
if wantProxy {
|
||||
nfr, err = newNetfilterRunner(log.Printf)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating new netfilter runner: %v", err)
|
||||
}
|
||||
}
|
||||
for {
|
||||
n, err := w.Next()
|
||||
if err != nil {
|
||||
@@ -326,7 +315,7 @@ authLoop:
|
||||
ipsHaveChanged := newCurrentIPs != currentIPs
|
||||
if cfg.ProxyTo != "" && len(addrs) > 0 && ipsHaveChanged {
|
||||
log.Printf("Installing proxy rules")
|
||||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs, nfr); err != nil {
|
||||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs); err != nil {
|
||||
log.Fatalf("installing ingress proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -341,7 +330,7 @@ authLoop:
|
||||
}
|
||||
}
|
||||
if cfg.TailnetTargetIP != "" && ipsHaveChanged && len(addrs) > 0 {
|
||||
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs, nfr); err != nil {
|
||||
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs); err != nil {
|
||||
log.Fatalf("installing egress proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -396,20 +385,19 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
||||
panic("cd must not be nil")
|
||||
}
|
||||
var tickChan <-chan time.Time
|
||||
var eventChan <-chan fsnotify.Event
|
||||
if w, err := fsnotify.NewWatcher(); err != nil {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Printf("failed to create fsnotify watcher, timer-only mode: %v", err)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
tickChan = ticker.C
|
||||
} else {
|
||||
defer w.Close()
|
||||
if err := w.Add(filepath.Dir(path)); err != nil {
|
||||
log.Fatalf("failed to add fsnotify watch: %v", err)
|
||||
}
|
||||
eventChan = w.Events
|
||||
}
|
||||
|
||||
if err := w.Add(filepath.Dir(path)); err != nil {
|
||||
log.Fatalf("failed to add fsnotify watch: %v", err)
|
||||
}
|
||||
var certDomain string
|
||||
var prevServeConfig *ipn.ServeConfig
|
||||
for {
|
||||
@@ -419,7 +407,7 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
||||
case <-cdChanged:
|
||||
certDomain = *certDomainAtomic.Load()
|
||||
case <-tickChan:
|
||||
case <-eventChan:
|
||||
case <-w.Events:
|
||||
// We can't do any reasonable filtering on the event because of how
|
||||
// k8s handles these mounts. So just re-read the file and apply it
|
||||
// if it's changed.
|
||||
@@ -540,40 +528,29 @@ func tailscaledArgs(cfg *settings) []string {
|
||||
return args
|
||||
}
|
||||
|
||||
// tailscaleUp uses cfg to run 'tailscale up' everytime containerboot starts, or
|
||||
// if TS_AUTH_ONCE is set, only the first time containerboot starts.
|
||||
func tailscaleUp(ctx context.Context, cfg *settings) error {
|
||||
args := []string{"--socket=" + cfg.Socket, "up"}
|
||||
if cfg.AcceptDNS {
|
||||
args = append(args, "--accept-dns=true")
|
||||
} else {
|
||||
args = append(args, "--accept-dns=false")
|
||||
}
|
||||
// tailscaleLogin uses cfg to run 'tailscale login' everytime containerboot
|
||||
// starts, or if TS_AUTH_ONCE is set, only the first time containerboot starts.
|
||||
func tailscaleLogin(ctx context.Context, cfg *settings) error {
|
||||
args := []string{"--socket=" + cfg.Socket, "login"}
|
||||
if cfg.AuthKey != "" {
|
||||
args = append(args, "--authkey="+cfg.AuthKey)
|
||||
}
|
||||
if cfg.Routes != "" {
|
||||
args = append(args, "--advertise-routes="+cfg.Routes)
|
||||
}
|
||||
if cfg.Hostname != "" {
|
||||
args = append(args, "--hostname="+cfg.Hostname)
|
||||
}
|
||||
if cfg.ExtraArgs != "" {
|
||||
args = append(args, strings.Fields(cfg.ExtraArgs)...)
|
||||
}
|
||||
log.Printf("Running 'tailscale up'")
|
||||
log.Printf("Running 'tailscale login'")
|
||||
cmd := exec.CommandContext(ctx, "tailscale", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("tailscale up failed: %v", err)
|
||||
return fmt.Errorf("tailscale login failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tailscaleSet uses cfg to run 'tailscale set' to set any known configuration
|
||||
// options that are passed in via environment variables. This is run after the
|
||||
// node is in Running state and only if TS_AUTH_ONCE is set.
|
||||
// node is in Running state.
|
||||
func tailscaleSet(ctx context.Context, cfg *settings) error {
|
||||
args := []string{"--socket=" + cfg.Socket, "set"}
|
||||
if cfg.AcceptDNS {
|
||||
@@ -685,12 +662,16 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, routes string
|
||||
return nil
|
||||
}
|
||||
|
||||
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var local netip.Addr
|
||||
argv0 := "iptables"
|
||||
if dst.Is6() {
|
||||
argv0 = "ip6tables"
|
||||
}
|
||||
var local string
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
@@ -698,30 +679,52 @@ func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []net
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr()
|
||||
local = pfx.Addr().String()
|
||||
break
|
||||
}
|
||||
if !local.IsValid() {
|
||||
if local == "" {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
|
||||
}
|
||||
if err := nfr.DNATNonTailscaleTraffic("tailscale0", dst); err != nil {
|
||||
return fmt.Errorf("installing egress proxy rules: %w", err)
|
||||
// Technically, if the control server ever changes the IPs assigned to this
|
||||
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
|
||||
// for now we'll live with it.
|
||||
// Set up a rule that ensures that all packets
|
||||
// except for those received on tailscale0 interface is forwarded to
|
||||
// destination address
|
||||
cmdDNAT := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "!", "-i", "tailscale0", "-j", "DNAT", "--to-destination", dstStr)
|
||||
cmdDNAT.Stdout = os.Stdout
|
||||
cmdDNAT.Stderr = os.Stderr
|
||||
if err := cmdDNAT.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
}
|
||||
if err := nfr.AddSNATRuleForDst(local, dst); err != nil {
|
||||
return fmt.Errorf("installing egress proxy rules: %w", err)
|
||||
// Set up a rule that ensures that all packets sent to the destination
|
||||
// address will have the proxy's IP set as source IP
|
||||
cmdSNAT := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "POSTROUTING", "1", "--destination", dstStr, "-j", "SNAT", "--to-source", local)
|
||||
cmdSNAT.Stdout = os.Stdout
|
||||
cmdSNAT.Stderr = os.Stderr
|
||||
if err := cmdSNAT.Run(); err != nil {
|
||||
return fmt.Errorf("setting up SNAT via iptables failed: %w", err)
|
||||
}
|
||||
if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
|
||||
return fmt.Errorf("installing egress proxy rules: %w", err)
|
||||
|
||||
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
|
||||
cmdClamp.Stdout = os.Stdout
|
||||
cmdClamp.Stderr = os.Stderr
|
||||
if err := cmdClamp.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var local netip.Addr
|
||||
argv0 := "iptables"
|
||||
if dst.Is6() {
|
||||
argv0 = "ip6tables"
|
||||
}
|
||||
var local string
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
@@ -729,17 +732,26 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr()
|
||||
local = pfx.Addr().String()
|
||||
break
|
||||
}
|
||||
if !local.IsValid() {
|
||||
if local == "" {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
|
||||
}
|
||||
if err := nfr.AddDNATRule(local, dst); err != nil {
|
||||
return fmt.Errorf("installing ingress proxy rules: %w", err)
|
||||
// Technically, if the control server ever changes the IPs assigned to this
|
||||
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
|
||||
// for now we'll live with it.
|
||||
cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
}
|
||||
if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
|
||||
return fmt.Errorf("installing ingress proxy rules: %w", err)
|
||||
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
|
||||
cmdClamp.Stdout = os.Stdout
|
||||
cmdClamp.Stderr = os.Stderr
|
||||
if err := cmdClamp.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,16 +129,22 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
|
||||
Name: "no_args",
|
||||
Env: nil,
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -146,17 +152,21 @@ func TestContainerBoot(t *testing.T) {
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -164,17 +174,21 @@ func TestContainerBoot(t *testing.T) {
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey-old-flag",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -183,30 +197,35 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "routes",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -215,6 +234,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -224,12 +246,13 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -238,6 +261,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -247,12 +273,13 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1::/64",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -261,6 +288,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=::/64,1::/64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -270,12 +300,13 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1.2.3.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -284,6 +315,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=::/64,1.2.3.0/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,16 +327,22 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_DEST_IP": "1.2.3.4",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
|
||||
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -312,16 +352,23 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_TAILNET_TARGET_IP": "100.99.99.99",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
"/usr/bin/iptables -t nat -I PREROUTING 1 ! -i tailscale0 -j DNAT --to-destination 100.99.99.99",
|
||||
"/usr/bin/iptables -t nat -I POSTROUTING 1 --destination 100.99.99.99 -j SNAT --to-source 100.64.0.1",
|
||||
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -342,7 +389,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
State: ptr.To(ipn.NeedsLogin),
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -358,6 +405,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -366,7 +414,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -374,6 +422,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
@@ -392,18 +443,22 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_KUBE_SECRET": "",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
@@ -414,6 +469,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
KubeDenyPatch: true,
|
||||
@@ -421,12 +477,15 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
@@ -456,7 +515,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
State: ptr.To(ipn.NeedsLogin),
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -480,6 +539,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -488,7 +548,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -496,6 +556,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
@@ -528,16 +591,20 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_SOCKS5_SERVER": "localhost:1080",
|
||||
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -545,16 +612,20 @@ func TestContainerBoot(t *testing.T) {
|
||||
Name: "dns",
|
||||
Env: map[string]string{
|
||||
"TS_ACCEPT_DNS": "true",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -563,31 +634,41 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_EXTRA_ARGS": "--widget=rotated",
|
||||
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --widget=rotated",
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hostname",
|
||||
Env: map[string]string{
|
||||
"TS_HOSTNAME": "my-server",
|
||||
"TS_HOSTNAME": "my-server",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --hostname=my-server",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --hostname=my-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -613,7 +694,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
|
||||
fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
|
||||
fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
|
||||
fmt.Sprint("TS_TEST_FAKE_NETFILTER=true"),
|
||||
}
|
||||
for k, v := range test.Env {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
|
||||
@@ -2,6 +2,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -12,6 +17,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
@@ -38,11 +44,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
|
||||
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
|
||||
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
@@ -78,6 +79,22 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
nhooyr.io/websocket from tailscale.com/cmd/derper+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
@@ -111,7 +128,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
@@ -147,11 +164,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/multierr from tailscale.com/health+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestProdAutocertHostPolicy(t *testing.T) {
|
||||
@@ -129,14 +128,3 @@ func TestNoContent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -192,15 +192,8 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
}
|
||||
}
|
||||
addIngressBackend(ing.Spec.DefaultBackend, "/")
|
||||
|
||||
var tlsHost string // hostname or FQDN or empty
|
||||
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
|
||||
tlsHost = ing.Spec.TLS[0].Hosts[0]
|
||||
}
|
||||
for _, rule := range ing.Spec.Rules {
|
||||
// Host is optional, but if it's present it must match the TLS host
|
||||
// otherwise we ignore the rule.
|
||||
if rule.Host != "" && rule.Host != tlsHost {
|
||||
if rule.Host != "" {
|
||||
a.recorder.Eventf(ing, corev1.EventTypeWarning, "InvalidIngressBackend", "rule with host %q ignored, unsupported", rule.Host)
|
||||
continue
|
||||
}
|
||||
@@ -215,8 +208,8 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
tags = strings.Split(tstr, ",")
|
||||
}
|
||||
hostname := ing.Namespace + "-" + ing.Name + "-ingress"
|
||||
if tlsHost != "" {
|
||||
hostname, _, _ = strings.Cut(tlsHost, ".")
|
||||
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
|
||||
hostname, _, _ = strings.Cut(ing.Spec.TLS[0].Hosts[0], ".")
|
||||
}
|
||||
|
||||
sts := &tailscaleSTSConfig{
|
||||
|
||||
@@ -151,10 +151,8 @@ spec:
|
||||
value: tailscale/tailscale:unstable
|
||||
- name: PROXY_TAGS
|
||||
value: tag:k8s
|
||||
- name: APISERVER_PROXY
|
||||
- name: AUTH_PROXY
|
||||
value: "false"
|
||||
- name: PROXY_FIREWALL_MODE
|
||||
value: auto
|
||||
volumeMounts:
|
||||
- name: oauth
|
||||
mountPath: /oauth
|
||||
|
||||
@@ -12,6 +12,7 @@ spec:
|
||||
serviceAccountName: proxies
|
||||
initContainers:
|
||||
- name: sysctler
|
||||
image: busybox
|
||||
securityContext:
|
||||
privileged: true
|
||||
command: ["/bin/sh"]
|
||||
|
||||
@@ -52,7 +52,6 @@ func main() {
|
||||
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
|
||||
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
|
||||
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
|
||||
tsFirewallMode = defaultEnv("PROXY_FIREWALL_MODE", "")
|
||||
)
|
||||
|
||||
var opts []kzap.Opts
|
||||
@@ -71,7 +70,7 @@ func main() {
|
||||
defer s.Close()
|
||||
restConfig := config.GetConfigOrDie()
|
||||
maybeLaunchAPIServerProxy(zlog, restConfig, s)
|
||||
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags, tsFirewallMode)
|
||||
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags)
|
||||
}
|
||||
|
||||
// initTSNet initializes the tsnet.Server and logs in to Tailscale. It uses the
|
||||
@@ -180,7 +179,7 @@ waitOnline:
|
||||
|
||||
// runReconcilers starts the controller-runtime manager and registers the
|
||||
// ServiceReconciler. It blocks forever.
|
||||
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags, tsFirewallMode string) {
|
||||
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags string) {
|
||||
var (
|
||||
isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false)
|
||||
)
|
||||
@@ -217,7 +216,6 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
proxyPriorityClassName: priorityClassName,
|
||||
tsFirewallMode: tsFirewallMode,
|
||||
}
|
||||
err = builder.
|
||||
ControllerManagedBy(mgr).
|
||||
@@ -230,7 +228,6 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
Client: mgr.GetClient(),
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
isDefaultLoadBalancer: isDefaultLoadBalancer,
|
||||
recorder: eventRecorder,
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create controller: %v", err)
|
||||
|
||||
@@ -70,12 +70,7 @@ func TestLoadBalancerClass(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
@@ -207,13 +202,7 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
tailnetTargetIP: tailnetTargetIP,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedEgressSTS(shortName, fullName, tailnetTargetIP, "default-test", ""))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -237,13 +226,7 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
|
||||
expectEqual(t, fc, want)
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o = stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
tailnetTargetIP: tailnetTargetIP,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedEgressSTS(shortName, fullName, tailnetTargetIP, "default-test", ""))
|
||||
|
||||
// Change the tailscale-target-ip annotation which should update the
|
||||
// StatefulSet
|
||||
@@ -322,12 +305,7 @@ func TestAnnotations(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -427,12 +405,7 @@ func TestAnnotationIntoLB(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, since it would have normally happened at
|
||||
@@ -477,12 +450,7 @@ func TestAnnotationIntoLB(t *testing.T) {
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
// None of the proxy machinery should have changed...
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o = stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
// ... but the service should have a LoadBalancer status.
|
||||
|
||||
want = &corev1.Service{
|
||||
@@ -560,12 +528,7 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
@@ -628,12 +591,7 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o = stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -703,12 +661,7 @@ func TestCustomHostname(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "reindeer-flotilla",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla", ""))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -782,7 +735,7 @@ func TestCustomPriorityClassName(t *testing.T) {
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
proxyPriorityClassName: "custom-priority-class-name",
|
||||
proxyPriorityClassName: "tailscale-critical",
|
||||
},
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
@@ -799,7 +752,7 @@ func TestCustomPriorityClassName(t *testing.T) {
|
||||
UID: types.UID("1234-UID"),
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
"tailscale.com/hostname": "tailscale-critical",
|
||||
"tailscale.com/hostname": "custom-priority-class-name",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
@@ -811,14 +764,8 @@ func TestCustomPriorityClassName(t *testing.T) {
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "tailscale-critical",
|
||||
priorityClassName: "custom-priority-class-name",
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "custom-priority-class-name", "tailscale-critical"))
|
||||
}
|
||||
|
||||
func TestDefaultLoadBalancer(t *testing.T) {
|
||||
@@ -864,63 +811,7 @@ func TestDefaultLoadBalancer(t *testing.T) {
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
}
|
||||
|
||||
func TestProxyFirewallMode(t *testing.T) {
|
||||
fc := fake.NewFakeClient()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
ssr: &tailscaleSTSReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
tsFirewallMode: "nftables",
|
||||
},
|
||||
logger: zl.Sugar(),
|
||||
isDefaultLoadBalancer: true,
|
||||
}
|
||||
|
||||
// Create a service that we should manage, and check that the initial round
|
||||
// of objects looks right.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
o := stsOpts{
|
||||
name: shortName,
|
||||
secretName: fullName,
|
||||
hostname: "default-test",
|
||||
firewallMode: "nftables",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
|
||||
}
|
||||
|
||||
func expectedSecret(name string) *corev1.Secret {
|
||||
@@ -971,44 +862,14 @@ func expectedHeadlessService(name string) *corev1.Service {
|
||||
}
|
||||
}
|
||||
|
||||
func expectedSTS(opts stsOpts) *appsv1.StatefulSet {
|
||||
containerEnv := []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
|
||||
{Name: "TS_HOSTNAME", Value: opts.hostname},
|
||||
}
|
||||
annots := map[string]string{
|
||||
"tailscale.com/operator-last-set-hostname": opts.hostname,
|
||||
}
|
||||
if opts.tailnetTargetIP != "" {
|
||||
annots["tailscale.com/operator-last-set-ts-tailnet-target-ip"] = opts.tailnetTargetIP
|
||||
containerEnv = append(containerEnv, corev1.EnvVar{
|
||||
Name: "TS_TAILNET_TARGET_IP",
|
||||
Value: opts.tailnetTargetIP,
|
||||
})
|
||||
} else {
|
||||
containerEnv = append(containerEnv, corev1.EnvVar{
|
||||
Name: "TS_DEST_IP",
|
||||
Value: "10.20.30.40",
|
||||
})
|
||||
|
||||
annots["tailscale.com/operator-last-set-cluster-ip"] = "10.20.30.40"
|
||||
|
||||
}
|
||||
if opts.firewallMode != "" {
|
||||
containerEnv = append(containerEnv, corev1.EnvVar{
|
||||
Name: "TS_DEBUG_FIREWALL_MODE",
|
||||
Value: opts.firewallMode,
|
||||
})
|
||||
}
|
||||
func expectedSTS(stsName, secretName, hostname, priorityClassName string) *appsv1.StatefulSet {
|
||||
return &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StatefulSet",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: opts.name,
|
||||
Name: stsName,
|
||||
Namespace: "operator-ns",
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
@@ -1022,20 +883,23 @@ func expectedSTS(opts stsOpts) *appsv1.StatefulSet {
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
ServiceName: opts.name,
|
||||
ServiceName: stsName,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: annots,
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/operator-last-set-hostname": hostname,
|
||||
"tailscale.com/operator-last-set-cluster-ip": "10.20.30.40",
|
||||
},
|
||||
DeletionGracePeriodSeconds: ptr.To[int64](10),
|
||||
Labels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: "proxies",
|
||||
PriorityClassName: opts.priorityClassName,
|
||||
PriorityClassName: priorityClassName,
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "sysctler",
|
||||
Image: "tailscale/tailscale",
|
||||
Image: "busybox",
|
||||
Command: []string{"/bin/sh"},
|
||||
Args: []string{"-c", "sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1"},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
@@ -1047,7 +911,82 @@ func expectedSTS(opts stsOpts) *appsv1.StatefulSet {
|
||||
{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: containerEnv,
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_KUBE_SECRET", Value: secretName},
|
||||
{Name: "TS_HOSTNAME", Value: hostname},
|
||||
{Name: "TS_DEST_IP", Value: "10.20.30.40"},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
ImagePullPolicy: "Always",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
func expectedEgressSTS(stsName, secretName, tailnetTargetIP, hostname, priorityClassName string) *appsv1.StatefulSet {
|
||||
return &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StatefulSet",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: stsName,
|
||||
Namespace: "operator-ns",
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": "default",
|
||||
"tailscale.com/parent-resource-type": "svc",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
ServiceName: stsName,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/operator-last-set-hostname": hostname,
|
||||
"tailscale.com/operator-last-set-ts-tailnet-target-ip": tailnetTargetIP,
|
||||
},
|
||||
DeletionGracePeriodSeconds: ptr.To[int64](10),
|
||||
Labels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: "proxies",
|
||||
PriorityClassName: priorityClassName,
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "sysctler",
|
||||
Image: "busybox",
|
||||
Command: []string{"/bin/sh"},
|
||||
Args: []string{"-c", "sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1"},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_KUBE_SECRET", Value: secretName},
|
||||
{Name: "TS_HOSTNAME", Value: hostname},
|
||||
{Name: "TS_TAILNET_TARGET_IP", Value: tailnetTargetIP},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
@@ -1187,15 +1126,6 @@ func expectRequeue(t *testing.T, sr *ServiceReconciler, ns, name string) {
|
||||
}
|
||||
}
|
||||
|
||||
type stsOpts struct {
|
||||
name string
|
||||
secretName string
|
||||
hostname string
|
||||
priorityClassName string
|
||||
firewallMode string
|
||||
tailnetTargetIP string
|
||||
}
|
||||
|
||||
type fakeTSClient struct {
|
||||
sync.Mutex
|
||||
keyRequests []tailscale.KeyCapabilities
|
||||
|
||||
@@ -9,9 +9,7 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -81,14 +79,6 @@ type tailscaleSTSReconciler struct {
|
||||
operatorNamespace string
|
||||
proxyImage string
|
||||
proxyPriorityClassName string
|
||||
tsFirewallMode string
|
||||
}
|
||||
|
||||
func (sts tailscaleSTSReconciler) validate() error {
|
||||
if sts.tsFirewallMode != "" && !isValidFirewallMode(sts.tsFirewallMode) {
|
||||
return fmt.Errorf("invalid proxy firewall mode %s, valid modes are iptables, nftables or unset", sts.tsFirewallMode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsHTTPSEnabledOnTailnet reports whether HTTPS is enabled on the tailnet.
|
||||
@@ -151,16 +141,10 @@ func (a *tailscaleSTSReconciler) Cleanup(ctx context.Context, logger *zap.Sugare
|
||||
return false, fmt.Errorf("getting device info: %w", err)
|
||||
}
|
||||
if id != "" {
|
||||
logger.Debugf("deleting device %s from control", string(id))
|
||||
// TODO: handle case where the device is already deleted, but the secret
|
||||
// is still around.
|
||||
if err := a.tsClient.DeleteDevice(ctx, string(id)); err != nil {
|
||||
errResp := &tailscale.ErrResponse{}
|
||||
if ok := errors.As(err, errResp); ok && errResp.Status == http.StatusNotFound {
|
||||
logger.Debugf("device %s not found, likely because it has already been deleted from control", string(id))
|
||||
} else {
|
||||
return false, fmt.Errorf("deleting device: %w", err)
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("device %s deleted from control", string(id))
|
||||
return false, fmt.Errorf("deleting device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,13 +307,6 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
if err := yaml.Unmarshal(proxyYaml, &ss); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal proxy spec: %w", err)
|
||||
}
|
||||
for i := range ss.Spec.Template.Spec.InitContainers {
|
||||
c := &ss.Spec.Template.Spec.InitContainers[i]
|
||||
if c.Name == "sysctler" {
|
||||
c.Image = a.proxyImage
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
container := &ss.Spec.Template.Spec.Containers[0]
|
||||
container.Image = a.proxyImage
|
||||
@@ -376,13 +353,6 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
},
|
||||
})
|
||||
}
|
||||
if a.tsFirewallMode != "" {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: "TS_DEBUG_FIREWALL_MODE",
|
||||
Value: a.tsFirewallMode,
|
||||
},
|
||||
)
|
||||
}
|
||||
ss.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: headlessSvc.Name,
|
||||
Namespace: a.operatorNamespace,
|
||||
@@ -522,7 +492,3 @@ func nameForService(svc *corev1.Service) (string, error) {
|
||||
}
|
||||
return svc.Namespace + "-" + svc.Name, nil
|
||||
}
|
||||
|
||||
func isValidFirewallMode(m string) bool {
|
||||
return m == "auto" || m == "nftables" || m == "iptables"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -38,8 +37,6 @@ type ServiceReconciler struct {
|
||||
// managedEgressProxies is a set of all egress proxies that we're currently
|
||||
// managing. This is only used for metrics.
|
||||
managedEgressProxies set.Slice[types.UID]
|
||||
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -139,15 +136,6 @@ func (a *ServiceReconciler) maybeCleanup(ctx context.Context, logger *zap.Sugare
|
||||
// This function adds a finalizer to svc, ensuring that we can handle orderly
|
||||
// deprovisioning later.
|
||||
func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) error {
|
||||
// run for proxy config related validations here as opposed to running
|
||||
// them earlier. This is to prevent cleanup etc being blocked on a
|
||||
// misconfigured proxy param
|
||||
if err := a.ssr.validate(); err != nil {
|
||||
msg := fmt.Sprintf("unable to provision proxy resources: invalid config: %v", err)
|
||||
a.recorder.Event(svc, corev1.EventTypeWarning, "INVALIDCONFIG", msg)
|
||||
a.logger.Error(msg)
|
||||
return nil
|
||||
}
|
||||
hostname, err := nameForService(svc)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -75,7 +75,6 @@ func main() {
|
||||
wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
|
||||
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
|
||||
hostname = fs.String("hostname", "", "Hostname to register the service under")
|
||||
)
|
||||
|
||||
err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
|
||||
@@ -90,7 +89,6 @@ func main() {
|
||||
|
||||
var s server
|
||||
s.ts.Port = uint16(*wgPort)
|
||||
s.ts.Hostname = *hostname
|
||||
defer s.ts.Close()
|
||||
|
||||
lc, err := s.ts.LocalClient()
|
||||
|
||||
@@ -139,22 +139,11 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: localAPIAction("break-derp-conns"),
|
||||
ShortHelp: "break any open DERP connections from the daemon",
|
||||
},
|
||||
{
|
||||
Name: "pick-new-derp",
|
||||
Exec: localAPIAction("pick-new-derp"),
|
||||
ShortHelp: "switch to some other random DERP home region for a short time",
|
||||
},
|
||||
{
|
||||
Name: "force-netmap-update",
|
||||
Exec: localAPIAction("force-netmap-update"),
|
||||
ShortHelp: "force a full no-op netmap update (for load testing)",
|
||||
},
|
||||
{
|
||||
// TODO(bradfitz,maisem): eventually promote this out of debug
|
||||
Name: "reload-config",
|
||||
Exec: reloadConfig,
|
||||
ShortHelp: "reload config",
|
||||
},
|
||||
{
|
||||
Name: "control-knobs",
|
||||
Exec: debugControlKnobs,
|
||||
@@ -457,20 +446,6 @@ func localAPIAction(action string) func(context.Context, []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func reloadConfig(ctx context.Context, args []string) error {
|
||||
ok, err := localClient.ReloadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
printf("config reloaded\n")
|
||||
return nil
|
||||
}
|
||||
printf("config mode not in use\n")
|
||||
os.Exit(1)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func runEnv(ctx context.Context, args []string) error {
|
||||
for _, e := range os.Environ() {
|
||||
outln(e)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -28,11 +29,8 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
tsrate "tailscale.com/tstime/rate"
|
||||
"tailscale.com/util/quarantine"
|
||||
"tailscale.com/util/truncate"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -54,12 +52,12 @@ var fileCmd = &ffcli.Command{
|
||||
|
||||
type countingReader struct {
|
||||
io.Reader
|
||||
n atomic.Int64
|
||||
n atomic.Uint64
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(buf []byte) (int, error) {
|
||||
n, err := c.Reader.Read(buf)
|
||||
c.n.Add(int64(n))
|
||||
c.n.Add(uint64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -172,100 +170,75 @@ func runCp(ctx context.Context, args []string) error {
|
||||
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
|
||||
}
|
||||
|
||||
var group syncs.WaitGroup
|
||||
ctxProgress, cancelProgress := context.WithCancel(ctx)
|
||||
defer cancelProgress()
|
||||
var (
|
||||
done = make(chan struct{}, 1)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
if isatty.IsTerminal(os.Stderr.Fd()) {
|
||||
group.Go(func() { progressPrinter(ctxProgress, name, fileContents.n.Load, contentLength) })
|
||||
go printProgress(&wg, done, fileContents, name, contentLength)
|
||||
wg.Add(1)
|
||||
}
|
||||
|
||||
err := localClient.PushFile(ctx, stableID, contentLength, name, fileContents)
|
||||
cancelProgress()
|
||||
group.Wait() // wait for progress printer to stop before reporting the error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sent %q", name)
|
||||
}
|
||||
done <- struct{}{}
|
||||
wg.Wait()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func progressPrinter(ctx context.Context, name string, contentCount func() int64, contentLength int64) {
|
||||
var rateValueFast, rateValueSlow tsrate.Value
|
||||
rateValueFast.HalfLife = 1 * time.Second // fast response for rate measurement
|
||||
rateValueSlow.HalfLife = 10 * time.Second // slow response for ETA measurement
|
||||
var prevContentCount int64
|
||||
print := func() {
|
||||
currContentCount := contentCount()
|
||||
rateValueFast.Add(float64(currContentCount - prevContentCount))
|
||||
rateValueSlow.Add(float64(currContentCount - prevContentCount))
|
||||
prevContentCount = currContentCount
|
||||
const vtRestartLine = "\r\x1b[K"
|
||||
|
||||
const vtRestartLine = "\r\x1b[K"
|
||||
fmt.Fprintf(os.Stderr, "%s%s %s %s",
|
||||
vtRestartLine,
|
||||
rightPad(name, 36),
|
||||
leftPad(formatIEC(float64(currContentCount), "B"), len("1023.00MiB")),
|
||||
leftPad(formatIEC(rateValueFast.Rate(), "B/s"), len("1023.00MiB/s")))
|
||||
if contentLength >= 0 {
|
||||
currContentCount = min(currContentCount, contentLength) // cap at 100%
|
||||
ratioRemain := float64(currContentCount) / float64(contentLength)
|
||||
bytesRemain := float64(contentLength - currContentCount)
|
||||
secsRemain := bytesRemain / rateValueSlow.Rate()
|
||||
secs := int(min(max(0, secsRemain), 99*60*60+59+60+59))
|
||||
fmt.Fprintf(os.Stderr, " %s %s",
|
||||
leftPad(fmt.Sprintf("%0.2f%%", 100.0*ratioRemain), len("100.00%")),
|
||||
fmt.Sprintf("ETA %02d:%02d:%02d", secs/60/60, (secs/60)%60, secs%60))
|
||||
}
|
||||
}
|
||||
func printProgress(wg *sync.WaitGroup, done <-chan struct{}, r *countingReader, name string, contentLength int64) {
|
||||
defer wg.Done()
|
||||
var lastBytesRead uint64
|
||||
|
||||
tc := time.NewTicker(250 * time.Millisecond)
|
||||
defer tc.Stop()
|
||||
print()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
print()
|
||||
case <-done:
|
||||
fmt.Fprintln(os.Stderr)
|
||||
return
|
||||
case <-tc.C:
|
||||
print()
|
||||
case <-time.After(time.Second):
|
||||
n := r.n.Load()
|
||||
contentLengthStr := "???"
|
||||
if contentLength > 0 {
|
||||
contentLengthStr = fmt.Sprint(contentLength / 1024)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s%s\t\t%s", vtRestartLine, padTruncateString(name, 36), padTruncateString(fmt.Sprintf("%d/%s kb", n/1024, contentLengthStr), 16))
|
||||
if contentLength > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\t%.02f%%", float64(n)/float64(contentLength)*100)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "\t-------%%")
|
||||
}
|
||||
if lastBytesRead > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\t%d kb/s", (n-lastBytesRead)/1024)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "\t-------")
|
||||
}
|
||||
lastBytesRead = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func leftPad(s string, n int) string {
|
||||
s = truncateString(s, n)
|
||||
return strings.Repeat(" ", max(n-len(s), 0)) + s
|
||||
}
|
||||
|
||||
func rightPad(s string, n int) string {
|
||||
s = truncateString(s, n)
|
||||
return s + strings.Repeat(" ", max(n-len(s), 0))
|
||||
}
|
||||
|
||||
func truncateString(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
func padTruncateString(str string, truncateAt int) string {
|
||||
if len(str) <= truncateAt {
|
||||
return str + strings.Repeat(" ", truncateAt-len(str))
|
||||
}
|
||||
return truncate.String(s, max(n-1, 0)) + "…"
|
||||
}
|
||||
|
||||
func formatIEC(n float64, unit string) string {
|
||||
switch {
|
||||
case n < 1<<10:
|
||||
return fmt.Sprintf("%0.2f%s", n/(1<<0), unit)
|
||||
case n < 1<<20:
|
||||
return fmt.Sprintf("%0.2fKi%s", n/(1<<10), unit)
|
||||
case n < 1<<30:
|
||||
return fmt.Sprintf("%0.2fMi%s", n/(1<<20), unit)
|
||||
case n < 1<<40:
|
||||
return fmt.Sprintf("%0.2fGi%s", n/(1<<30), unit)
|
||||
default:
|
||||
return fmt.Sprintf("%0.2fTi%s", n/(1<<40), unit)
|
||||
// Truncate the string, but respect unicode codepoint boundaries.
|
||||
// As of RFC3629 utf-8 codepoints can be at most 4 bytes wide.
|
||||
for i := 1; i <= 4 && i < len(str)-truncateAt; i++ {
|
||||
if utf8.ValidString(str[:truncateAt-i]) {
|
||||
return str[:truncateAt-i] + "…"
|
||||
}
|
||||
}
|
||||
return "" // Should be unreachable
|
||||
}
|
||||
|
||||
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
|
||||
|
||||
@@ -13,17 +13,22 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
var funnelCmd = func() *ffcli.Command {
|
||||
se := &serveEnv{lc: &localClient}
|
||||
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
||||
// change is limited to make a revert easier and full cleanup to come after the relase.
|
||||
// TODO(tylersmalley): cleanup and removal of newFunnelCommand as of 2023-10-16
|
||||
return newServeV2Command(se, funnel)
|
||||
// This flag is used to switch to an in-development
|
||||
// implementation of the tailscale funnel command.
|
||||
// See https://github.com/tailscale/tailscale/issues/7844
|
||||
if envknob.UseWIPCode() {
|
||||
return newServeDevCommand(se, funnel)
|
||||
}
|
||||
return newFunnelCommand(se)
|
||||
}
|
||||
|
||||
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
|
||||
@@ -87,6 +92,10 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||
if err != nil {
|
||||
@@ -98,15 +107,11 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
// Don't block from turning off existing Funnel if
|
||||
// network configuration/capabilities have changed.
|
||||
// Only block from starting new Funnels.
|
||||
if err := e.verifyFunnelEnabled(ctx, port); err != nil {
|
||||
if err := e.verifyFunnelEnabled(ctx, st, port); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
@@ -140,7 +145,13 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
// If an error is reported, the CLI should stop execution and return the error.
|
||||
//
|
||||
// verifyFunnelEnabled may refresh the local state and modify the st input.
|
||||
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, port uint16) error {
|
||||
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status, port uint16) error {
|
||||
hasFunnelAttrs := func(selfNode *ipnstate.PeerStatus) bool {
|
||||
return selfNode.HasCap(tailcfg.CapabilityHTTPS) && selfNode.HasCap(tailcfg.NodeAttrFunnel)
|
||||
}
|
||||
if hasFunnelAttrs(st.Self) {
|
||||
return nil // already enabled
|
||||
}
|
||||
enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel)
|
||||
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
|
||||
switch {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -33,14 +34,17 @@ import (
|
||||
|
||||
var serveCmd = func() *ffcli.Command {
|
||||
se := &serveEnv{lc: &localClient}
|
||||
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
||||
// change is limited to make a revert easier and full cleanup to come after the relase.
|
||||
// TODO(tylersmalley): cleanup and removal of newServeLegacyCommand as of 2023-10-16
|
||||
return newServeV2Command(se, serve)
|
||||
// This flag is used to switch to an in-development
|
||||
// implementation of the tailscale funnel command.
|
||||
// See https://github.com/tailscale/tailscale/issues/7844
|
||||
if envknob.UseWIPCode() {
|
||||
return newServeDevCommand(se, serve)
|
||||
}
|
||||
return newServeCommand(se)
|
||||
}
|
||||
|
||||
// newServeLegacyCommand returns a new "serve" subcommand using e as its environment.
|
||||
func newServeLegacyCommand(e *serveEnv) *ffcli.Command {
|
||||
// newServeCommand returns a new "serve" subcommand using e as its environment.
|
||||
func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "serve",
|
||||
ShortHelp: "Serve content and local servers",
|
||||
@@ -159,19 +163,17 @@ type serveEnv struct {
|
||||
// v2 specific flags
|
||||
bg bool // background mode
|
||||
setPath string // serve path
|
||||
https uint // HTTP port
|
||||
http uint // HTTP port
|
||||
tcp uint // TCP port
|
||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||
https string // HTTP port
|
||||
http string // HTTP port
|
||||
tcp string // TCP port
|
||||
tlsTerminatedTCP string // a TLS terminated TCP port
|
||||
subcmd serveMode // subcommand
|
||||
yes bool // update without prompt
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
||||
// optional stuff for tests:
|
||||
testFlagOut io.Writer
|
||||
testStdout io.Writer
|
||||
testStderr io.Writer
|
||||
}
|
||||
|
||||
// getSelfDNSName returns the DNS name of the current node.
|
||||
@@ -682,6 +684,13 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) stdout() io.Writer {
|
||||
if e.testStdout != nil {
|
||||
return e.testStdout
|
||||
}
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error {
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
for p, h := range sc.TCP {
|
||||
@@ -818,24 +827,6 @@ func parseServePort(s string) (uint16, error) {
|
||||
// 2023-08-09: The only valid feature values are "serve" and "funnel".
|
||||
// This can be moved to some CLI lib when expanded past serve/funnel.
|
||||
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) {
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if st.Self == nil {
|
||||
return errors.New("no self node")
|
||||
}
|
||||
hasCaps := func() bool {
|
||||
for _, c := range caps {
|
||||
if !st.Self.HasCap(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if hasCaps() {
|
||||
return nil // already enabled
|
||||
}
|
||||
info, err := e.lc.QueryFeature(ctx, feature)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -5,20 +5,17 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -41,19 +38,15 @@ type commandInfo struct {
|
||||
}
|
||||
|
||||
var serveHelpCommon = strings.TrimSpace(`
|
||||
<target> can be a file, directory, text, or most commonly the location to a service running on the
|
||||
local machine. The location to the location service can be expressed as a port number (e.g., 3000),
|
||||
a partial URL (e.g., localhost:3000), or a full URL including a path (e.g., http://localhost:3000/foo).
|
||||
<target> can be a port number (e.g., 3000), a partial URL (e.g., localhost:3000), or a
|
||||
full URL including a path (e.g., http://localhost:3000/foo, https+insecure://localhost:3000/foo).
|
||||
|
||||
EXAMPLES
|
||||
- Expose an HTTP server running at 127.0.0.1:3000 in the foreground:
|
||||
$ tailscale %[1]s 3000
|
||||
- Mount a local web server at 127.0.0.1:3000 in the foreground:
|
||||
$ tailscale %s localhost:3000
|
||||
|
||||
- Expose an HTTP server running at 127.0.0.1:3000 in the background:
|
||||
$ tailscale %[1]s --bg 3000
|
||||
|
||||
- Expose an HTTPS server with invalid or self-signed certificates at https://localhost:8443
|
||||
$ tailscale %[1]s https+insecure://localhost:8443
|
||||
- Mount a local web server at 127.0.0.1:3000 in the background:
|
||||
$ tailscale %s --bg localhost:3000
|
||||
|
||||
For more examples and use cases visit our docs site https://tailscale.com/kb/1247/funnel-serve-use-cases
|
||||
`)
|
||||
@@ -79,7 +72,7 @@ var infoMap = map[serveMode]commandInfo{
|
||||
Name: "serve",
|
||||
ShortHelp: "Serve content and local servers on your tailnet",
|
||||
LongHelp: strings.Join([]string{
|
||||
"Tailscale Serve enables you to share a local server securely within your tailnet.\n",
|
||||
"Serve enables you to share a local server securely within your tailnet.\n",
|
||||
"To share a local server on the internet, use `tailscale funnel`\n\n",
|
||||
}, "\n"),
|
||||
},
|
||||
@@ -101,14 +94,8 @@ func buildShortUsage(subcmd string) string {
|
||||
}, "\n ")
|
||||
}
|
||||
|
||||
// errHelpFunc is standard error text that prompts users to
|
||||
// run `$subcmd --help` for information on how to use serve.
|
||||
var errHelpFunc = func(m serveMode) error {
|
||||
return fmt.Errorf("try `tailscale %s --help` for usage info", infoMap[m].Name)
|
||||
}
|
||||
|
||||
// newServeV2Command returns a new "serve" subcommand using e as its environment.
|
||||
func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
// newServeDevCommand returns a new "serve" subcommand using e as its environment.
|
||||
func newServeDevCommand(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
if subcmd != serve && subcmd != funnel {
|
||||
log.Fatalf("newServeDevCommand called with unknown subcmd %q", subcmd)
|
||||
}
|
||||
@@ -123,21 +110,19 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
fmt.Sprintf("%s status [--json]", info.Name),
|
||||
fmt.Sprintf("%s reset", info.Name),
|
||||
}, "\n "),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name, info.Name),
|
||||
Exec: e.runServeCombined(subcmd),
|
||||
|
||||
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.bg, "bg", false, "Run the command as a background process (default false)")
|
||||
fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service")
|
||||
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
||||
if subcmd == serve {
|
||||
fs.UintVar(&e.http, "http", 0, "Expose an HTTP server at the specified port")
|
||||
}
|
||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)")
|
||||
fs.BoolVar(&e.bg, "bg", false, "run the command in the background")
|
||||
fs.StringVar(&e.setPath, "set-path", "", "set a path for a specific target and run in the background")
|
||||
fs.StringVar(&e.https, "https", "", "default; HTTPS listener")
|
||||
fs.StringVar(&e.http, "http", "", "HTTP listener")
|
||||
fs.StringVar(&e.tcp, "tcp", "", "TCP listener")
|
||||
fs.StringVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", "", "TLS terminated TCP listener")
|
||||
|
||||
}),
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
UsageFunc: usageFunc,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
@@ -159,31 +144,20 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
|
||||
if translation, ok := isLegacyInvocation(subcmd, args); ok {
|
||||
fmt.Fprint(e.stderr(), "Error: the CLI for serve and funnel has changed.")
|
||||
if translation != "" {
|
||||
fmt.Fprint(e.stderr(), " You can run the following command instead:\n")
|
||||
fmt.Fprintf(e.stderr(), "\t- %s\n", translation)
|
||||
}
|
||||
fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
if len(args) == 0 {
|
||||
func validateArgs(subcmd serveMode, args []string) error {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return flag.ErrHelp
|
||||
case 1, 2:
|
||||
if isLegacyInvocation(subcmd, args) {
|
||||
fmt.Fprintf(os.Stderr, "error: the CLI for serve and funnel has changed.")
|
||||
fmt.Fprintf(os.Stderr, "Please see https://tailscale.com/kb/1242/tailscale-serve for more information.")
|
||||
return errHelp
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments (%d)", len(args))
|
||||
return errHelp
|
||||
}
|
||||
if len(args) > 2 {
|
||||
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
turnOff := args[len(args)-1] == "off"
|
||||
if len(args) == 2 && !turnOff {
|
||||
fmt.Fprintln(e.stderr(), "Error: invalid argument format")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
// Given the two checks above, we can assume there
|
||||
// are only 1 or 2 arguments which is valid.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -192,31 +166,22 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
e.subcmd = subcmd
|
||||
|
||||
return func(ctx context.Context, args []string) error {
|
||||
// Undocumented debug command (not using ffcli subcommands) to set raw
|
||||
// configs from stdin for now (2022-11-13).
|
||||
if len(args) == 1 && args[0] == "set-raw" {
|
||||
valb, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := new(ipn.ServeConfig)
|
||||
if err := json.Unmarshal(valb, sc); err != nil {
|
||||
return fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
if err := e.validateArgs(subcmd, args); err != nil {
|
||||
if err := validateArgs(subcmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
funnel := subcmd == funnel
|
||||
if funnel {
|
||||
// verify node has funnel capabilities
|
||||
if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
|
||||
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -226,10 +191,18 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
}
|
||||
|
||||
if e.setPath != "" {
|
||||
// TODO(marwan-at-work): either
|
||||
// 1. Warn the user that this is a side effect.
|
||||
// 2. Force the user to pass --bg
|
||||
// 3. Allow set-path to be in the foreground.
|
||||
e.bg = true
|
||||
}
|
||||
|
||||
srvType, srvPort, err := srvTypeAndPortFromFlags(e)
|
||||
if err != nil {
|
||||
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
|
||||
return errHelpFunc(subcmd)
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
@@ -241,10 +214,6 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
|
||||
// set parent serve config to always be persisted
|
||||
@@ -270,13 +239,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
|
||||
var watcher *tailscale.IPNBusWatcher
|
||||
wantFg := !e.bg && !turnOff
|
||||
if wantFg {
|
||||
// validate the config before creating a WatchIPNBus session
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !e.bg && !turnOff {
|
||||
// if foreground mode, create a WatchIPNBus session
|
||||
// and use the nested config for all following operations
|
||||
// TODO(marwan-at-work): nested-config validations should happen here or previous to this point.
|
||||
@@ -305,22 +268,22 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return err
|
||||
}
|
||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
|
||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||
msg = e.messageForPort(sc, st, dnsName, srvPort)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
|
||||
return errHelpFunc(subcmd)
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
if err := e.lc.SetServeConfig(ctx, parentSC); err != nil {
|
||||
if tailscale.IsPreconditionsFailedError(err) {
|
||||
fmt.Fprintln(e.stderr(), "Another client is changing the serve config; please try again.")
|
||||
fmt.Fprintln(os.Stderr, "Another client is changing the serve config; please try again.")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
fmt.Fprintln(e.stdout(), msg)
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
}
|
||||
|
||||
if watcher != nil {
|
||||
@@ -339,8 +302,6 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration"
|
||||
|
||||
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
|
||||
sc, isFg := findConfig(sc, port)
|
||||
if sc == nil {
|
||||
@@ -350,7 +311,7 @@ func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe se
|
||||
return errors.New("foreground already exists under this port")
|
||||
}
|
||||
if !e.bg {
|
||||
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
|
||||
return errors.New("background serve already exists under this port")
|
||||
}
|
||||
existingServe := serveFromPortHandler(sc.TCP[port])
|
||||
if wantServe != existingServe {
|
||||
@@ -402,10 +363,6 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
||||
return fmt.Errorf("failed apply web serve: %w", err)
|
||||
}
|
||||
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
||||
if e.setPath != "" {
|
||||
return fmt.Errorf("cannot mount a path for TCP serve")
|
||||
}
|
||||
|
||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||
@@ -420,27 +377,18 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
msgFunnelAvailable = "Available on the internet:"
|
||||
msgServeAvailable = "Available within your tailnet:"
|
||||
msgRunningInBackground = "%s started and running in the background."
|
||||
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
|
||||
msgToExit = "Press Ctrl+C to exit."
|
||||
)
|
||||
|
||||
// messageForPort returns a message for the given port based on the
|
||||
// serve config and status.
|
||||
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16) string {
|
||||
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16) string {
|
||||
var output strings.Builder
|
||||
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
|
||||
if sc.AllowFunnel[hp] == true {
|
||||
output.WriteString(msgFunnelAvailable)
|
||||
output.WriteString("Available on the internet:\n")
|
||||
} else {
|
||||
output.WriteString(msgServeAvailable)
|
||||
output.WriteString("Available within your tailnet:\n")
|
||||
}
|
||||
output.WriteString("\n\n")
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(srvPort) {
|
||||
@@ -453,6 +401,13 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
portPart = ""
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n\n", scheme, dnsName, portPart))
|
||||
|
||||
if !e.bg {
|
||||
output.WriteString("Press Ctrl+C to exit.")
|
||||
return output.String()
|
||||
}
|
||||
|
||||
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||
switch {
|
||||
case h.Path != "":
|
||||
@@ -474,12 +429,12 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return len(mounts[i]) < len(mounts[j])
|
||||
})
|
||||
maxLen := len(mounts[len(mounts)-1])
|
||||
|
||||
for _, m := range mounts {
|
||||
h := sc.Web[hp].Handlers[m]
|
||||
t, d := srvTypeAndDesc(h)
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, dnsName, portPart, m))
|
||||
output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d))
|
||||
output.WriteString(fmt.Sprintf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d))
|
||||
}
|
||||
} else if sc.TCP[srvPort] != nil {
|
||||
h := sc.TCP[srvPort]
|
||||
@@ -489,7 +444,6 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
tlsStatus = "TLS terminated"
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n", scheme, dnsName, portPart))
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
|
||||
for _, a := range st.TailscaleIPs {
|
||||
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
|
||||
@@ -498,17 +452,8 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward))
|
||||
}
|
||||
|
||||
if !e.bg {
|
||||
output.WriteString(msgToExit)
|
||||
return output.String()
|
||||
}
|
||||
|
||||
subCmd := infoMap[e.subcmd].Name
|
||||
subCmdUpper := strings.ToUpper(string(subCmd[0])) + subCmd[1:]
|
||||
|
||||
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper))
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
|
||||
output.WriteString("\nServe started and running in the background.\n")
|
||||
output.WriteString(fmt.Sprintf("To disable the proxy, run: tailscale %s off", infoMap[e.subcmd].Name))
|
||||
|
||||
return output.String()
|
||||
}
|
||||
@@ -543,7 +488,7 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
}
|
||||
h.Path = target
|
||||
default:
|
||||
t, err := expandProxyTargetDev(target, []string{"http", "https", "https+insecure"}, "http")
|
||||
t, err := expandProxyTargetDev(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -593,22 +538,34 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
|
||||
return fmt.Errorf("invalid TCP target %q", target)
|
||||
}
|
||||
|
||||
targetURL, err := expandProxyTargetDev(target, []string{"tcp"}, "tcp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to expand target: %v", err)
|
||||
}
|
||||
|
||||
dstURL, err := url.Parse(targetURL)
|
||||
dstURL, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid TCP target %q: %v", target, err)
|
||||
}
|
||||
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid TCP target %q: %v", target, err)
|
||||
}
|
||||
|
||||
switch host {
|
||||
case "localhost", "127.0.0.1":
|
||||
// ok
|
||||
default:
|
||||
return fmt.Errorf("invalid TCP target %q, must be one of localhost or 127.0.0.1", target)
|
||||
}
|
||||
|
||||
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
|
||||
return fmt.Errorf("invalid port %q", dstPortStr)
|
||||
}
|
||||
|
||||
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||
|
||||
// TODO: needs to account for multiple configs from foreground mode
|
||||
if sc.IsServingWeb(srcPort) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: dstURL.Host})
|
||||
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
if terminateTLS {
|
||||
sc.TCP[srcPort].TerminateTLS = dnsName
|
||||
@@ -630,9 +587,6 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
|
||||
// TODO: add error handling for if toggling for existing sc
|
||||
if allowFunnel {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
} else if _, exists := sc.AllowFunnel[hp]; exists {
|
||||
fmt.Fprintf(e.stderr(), "Removing Funnel for %s\n", hp)
|
||||
delete(sc.AllowFunnel, hp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,7 +613,7 @@ func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serve
|
||||
}
|
||||
|
||||
func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, err error) {
|
||||
sourceMap := map[serveType]uint{
|
||||
sourceMap := map[serveType]string{
|
||||
serveTypeHTTP: e.http,
|
||||
serveTypeHTTPS: e.https,
|
||||
serveTypeTCP: e.tcp,
|
||||
@@ -667,15 +621,13 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
|
||||
}
|
||||
|
||||
var srcTypeCount int
|
||||
var srcValue string
|
||||
|
||||
for k, v := range sourceMap {
|
||||
if v != 0 {
|
||||
if v > math.MaxUint16 {
|
||||
return 0, 0, fmt.Errorf("port number %d is too high for %s flag", v, srvType)
|
||||
}
|
||||
if v != "" {
|
||||
srcTypeCount++
|
||||
srvType = k
|
||||
srvPort = uint16(v)
|
||||
srcValue = v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,104 +635,29 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
|
||||
return 0, 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||
} else if srcTypeCount == 0 {
|
||||
srvType = serveTypeHTTPS
|
||||
srvPort = 443
|
||||
srcValue = "443"
|
||||
}
|
||||
|
||||
srvPort, err = parseServePort(srcValue)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid port %q: %w", srcValue, err)
|
||||
}
|
||||
|
||||
return srvType, srvPort, nil
|
||||
}
|
||||
|
||||
// isLegacyInvocation helps transition customers who have been using the beta
|
||||
// CLI to the newer API by returning a translation from the old command to the new command.
|
||||
// The second result is a boolean that only returns true if the given arguments is a valid
|
||||
// legacy invocation. If the given args are in the old format but are not valid, it will
|
||||
// return false and expects the new code path has enough validations to reject the request.
|
||||
func isLegacyInvocation(subcmd serveMode, args []string) (string, bool) {
|
||||
if subcmd == funnel {
|
||||
if len(args) != 2 {
|
||||
return "", false
|
||||
}
|
||||
_, err := strconv.ParseUint(args[0], 10, 16)
|
||||
return "", err == nil && (args[1] == "on" || args[1] == "off")
|
||||
}
|
||||
turnOff := len(args) > 1 && args[len(args)-1] == "off"
|
||||
if turnOff {
|
||||
args = args[:len(args)-1]
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return "", false
|
||||
}
|
||||
func isLegacyInvocation(subcmd serveMode, args []string) bool {
|
||||
if subcmd == serve && len(args) == 2 {
|
||||
prefixes := []string{"http", "https", "tcp", "tls-terminated-tcp"}
|
||||
|
||||
srcType, srcPortStr, found := strings.Cut(args[0], ":")
|
||||
if !found {
|
||||
if srcType == "https" && srcPortStr == "" {
|
||||
// Default https port to 443.
|
||||
srcPortStr = "443"
|
||||
} else if srcType == "http" && srcPortStr == "" {
|
||||
// Default http port to 80.
|
||||
srcPortStr = "80"
|
||||
} else {
|
||||
return "", false
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(args[0], prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wantLength int
|
||||
switch srcType {
|
||||
case "https", "http":
|
||||
wantLength = 3
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
wantLength = 2
|
||||
default:
|
||||
// return non-legacy, and let new code handle validation.
|
||||
return "", false
|
||||
}
|
||||
// The length is either exactlly the same as in "https / <target>"
|
||||
// or target is omitted as in "https / off" where omit the off at
|
||||
// the top.
|
||||
if len(args) != wantLength && !(turnOff && len(args) == wantLength-1) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
cmd := []string{"tailscale", "serve", "--bg"}
|
||||
switch srcType {
|
||||
case "https":
|
||||
// In the new code, we default to https:443,
|
||||
// so we don't need to pass the flag explicitly.
|
||||
if srcPortStr != "443" {
|
||||
cmd = append(cmd, fmt.Sprintf("--https %s", srcPortStr))
|
||||
}
|
||||
case "http":
|
||||
cmd = append(cmd, fmt.Sprintf("--http %s", srcPortStr))
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
cmd = append(cmd, fmt.Sprintf("--%s %s", srcType, srcPortStr))
|
||||
}
|
||||
|
||||
var mount string
|
||||
if srcType == "https" || srcType == "http" {
|
||||
mount = args[1]
|
||||
if _, err := cleanMountPoint(mount); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if mount != "/" {
|
||||
cmd = append(cmd, "--set-path "+mount)
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no "off" there must always be a target destination.
|
||||
// If there is "off", target is optional so check if it exists
|
||||
// first before appending it.
|
||||
hasTarget := !turnOff || (turnOff && len(args) == wantLength)
|
||||
if hasTarget {
|
||||
dest := args[len(args)-1]
|
||||
if strings.Contains(dest, " ") {
|
||||
dest = strconv.Quote(dest)
|
||||
}
|
||||
cmd = append(cmd, dest)
|
||||
}
|
||||
if turnOff {
|
||||
cmd = append(cmd, "off")
|
||||
}
|
||||
|
||||
return strings.Join(cmd, " "), true
|
||||
return false
|
||||
}
|
||||
|
||||
// removeWebServe removes a web handler from the serve config
|
||||
@@ -792,43 +669,15 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
}
|
||||
|
||||
portStr := strconv.Itoa(int(srvPort))
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, portStr))
|
||||
|
||||
var targetExists bool
|
||||
var mounts []string
|
||||
// mount is deduced from e.setPath but it is ambiguous as
|
||||
// to whether the user explicitly passed "/" or it was defaulted to.
|
||||
if e.setPath == "" {
|
||||
targetExists = sc.Web[hp] != nil && len(sc.Web[hp].Handlers) > 0
|
||||
if targetExists {
|
||||
for mount := range sc.Web[hp].Handlers {
|
||||
mounts = append(mounts, mount)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
targetExists = sc.WebHandlerExists(hp, mount)
|
||||
mounts = []string{mount}
|
||||
}
|
||||
|
||||
if !targetExists {
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
if !sc.WebHandlerExists(hp, mount) {
|
||||
return errors.New("error: handler does not exist")
|
||||
}
|
||||
|
||||
if len(mounts) > 1 {
|
||||
msg := fmt.Sprintf("Are you sure you want to delete %d handlers under port %s?", len(mounts), portStr)
|
||||
if !e.yes && !promptYesNo(msg) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// delete existing handler, then cascade delete if empty
|
||||
for _, m := range mounts {
|
||||
delete(sc.Web[hp].Handlers, m)
|
||||
}
|
||||
delete(sc.Web[hp].Handlers, mount)
|
||||
if len(sc.Web[hp].Handlers) == 0 {
|
||||
delete(sc.Web, hp)
|
||||
delete(sc.AllowFunnel, hp)
|
||||
delete(sc.TCP, srvPort)
|
||||
}
|
||||
|
||||
@@ -846,10 +695,6 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
|
||||
delete(sc.AllowFunnel, hp)
|
||||
}
|
||||
|
||||
if len(sc.AllowFunnel) == 0 {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -880,22 +725,24 @@ func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
|
||||
// examples:
|
||||
// - 3000
|
||||
// - localhost:3000
|
||||
// - tcp://localhost:3000
|
||||
// - http://localhost:3000
|
||||
// - https://localhost:3000
|
||||
// - https-insecure://localhost:3000
|
||||
// - https-insecure://localhost:3000/foo
|
||||
func expandProxyTargetDev(target string, supportedSchemes []string, defaultScheme string) (string, error) {
|
||||
const host = "127.0.0.1"
|
||||
func expandProxyTargetDev(target string) (string, error) {
|
||||
var (
|
||||
scheme = "http"
|
||||
host = "127.0.0.1"
|
||||
)
|
||||
|
||||
// support target being a port number
|
||||
if port, err := strconv.ParseUint(target, 10, 16); err == nil {
|
||||
return fmt.Sprintf("%s://%s:%d", defaultScheme, host, port), nil
|
||||
return fmt.Sprintf("%s://%s:%d", scheme, host, port), nil
|
||||
}
|
||||
|
||||
// prepend scheme if not present
|
||||
if !strings.Contains(target, "://") {
|
||||
target = defaultScheme + "://" + target
|
||||
target = scheme + "://" + target
|
||||
}
|
||||
|
||||
// make sure we can parse the target
|
||||
@@ -905,15 +752,10 @@ func expandProxyTargetDev(target string, supportedSchemes []string, defaultSchem
|
||||
}
|
||||
|
||||
// ensure a supported scheme
|
||||
if !slices.Contains(supportedSchemes, u.Scheme) {
|
||||
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
|
||||
}
|
||||
|
||||
// validate the host.
|
||||
switch u.Hostname() {
|
||||
case "localhost", "127.0.0.1":
|
||||
switch u.Scheme {
|
||||
case "http", "https", "https+insecure":
|
||||
default:
|
||||
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
|
||||
return "", errors.New("must be a URL starting with http://, https://, or https+insecure://")
|
||||
}
|
||||
|
||||
// validate the port
|
||||
@@ -922,7 +764,13 @@ func expandProxyTargetDev(target string, supportedSchemes []string, defaultSchem
|
||||
return "", fmt.Errorf("invalid port %q", u.Port())
|
||||
}
|
||||
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
// validate the host.
|
||||
switch u.Hostname() {
|
||||
case "localhost", "127.0.0.1":
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
default:
|
||||
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
@@ -960,17 +808,3 @@ func (s serveType) String() string {
|
||||
return "unknownServeType"
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) stdout() io.Writer {
|
||||
if e.testStdout != nil {
|
||||
return e.testStdout
|
||||
}
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
func (e *serveEnv) stderr() io.Writer {
|
||||
if e.testStderr != nil {
|
||||
return e.testStderr
|
||||
}
|
||||
return os.Stderr
|
||||
}
|
||||
1060
cmd/tailscale/cli/serve_dev_test.go
Normal file
1060
cmd/tailscale/cli/serve_dev_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -713,7 +713,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
cmd = newFunnelCommand(e)
|
||||
args = st.command[1:]
|
||||
} else {
|
||||
cmd = newServeLegacyCommand(e)
|
||||
cmd = newServeCommand(e)
|
||||
args = st.command
|
||||
}
|
||||
err := cmd.ParseAndRun(context.Background(), args)
|
||||
@@ -786,7 +786,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
{
|
||||
name: "fallback-flow-enabled",
|
||||
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
||||
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"},
|
||||
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
|
||||
wantErr: "", // no error, success
|
||||
},
|
||||
{
|
||||
@@ -811,6 +811,10 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test
|
||||
fakeStatus.Self.Capabilities = tt.caps
|
||||
}
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
@@ -822,7 +826,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
t.Errorf("wrong panic; got=%s, want=%s", gotPanic, tt.wantPanic)
|
||||
}
|
||||
}()
|
||||
gotErr := e.verifyFunnelEnabled(ctx, 443)
|
||||
gotErr := e.verifyFunnelEnabled(ctx, st, 443)
|
||||
var got string
|
||||
if gotErr != nil {
|
||||
got = gotErr.Error()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/clientupdate"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var setCmd = &ffcli.Command{
|
||||
@@ -51,7 +49,6 @@ type setArgsT struct {
|
||||
forceDaemon bool
|
||||
updateCheck bool
|
||||
updateApply bool
|
||||
postureChecking bool
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
@@ -67,10 +64,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
setf.StringVar(&setArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "notify about available Tailscale updates")
|
||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
|
||||
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
|
||||
|
||||
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
|
||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
@@ -113,7 +108,6 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
Check: setArgs.updateCheck,
|
||||
Apply: setArgs.updateApply,
|
||||
},
|
||||
PostureChecking: setArgs.postureChecking,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -159,22 +153,9 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
}
|
||||
}
|
||||
if maskedPrefs.AutoUpdateSet {
|
||||
// On macsys, tailscaled will set the Sparkle auto-update setting. It
|
||||
// does not use clientupdate.
|
||||
if version.IsMacSysExt() {
|
||||
apply := "0"
|
||||
if maskedPrefs.AutoUpdate.Apply {
|
||||
apply = "1"
|
||||
}
|
||||
out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
||||
}
|
||||
} else {
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
}
|
||||
checkPrefs := curPrefs.Clone()
|
||||
|
||||
@@ -238,11 +238,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
printFunnelStatus(ctx)
|
||||
if cv := st.ClientVersion; cv != nil && !cv.RunningLatest && cv.LatestVersion != "" {
|
||||
if cv.UrgentSecurityUpdate {
|
||||
printf("# Security update available: %v -> %v, run `tailscale update` or `tailscale set --auto-update` to update.\n", version.Short(), cv.LatestVersion)
|
||||
} else {
|
||||
printf("# Update available: %v -> %v, run `tailscale update` or `tailscale set --auto-update` to update.\n", version.Short(), cv.LatestVersion)
|
||||
}
|
||||
printf("# Update available: %v -> %v, run `tailscale update` or `tailscale set --auto-update` to update.\n", version.Short(), cv.LatestVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -114,7 +114,6 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
@@ -533,11 +532,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
if cv != nil && !cv.RunningLatest && cv.LatestVersion != "" {
|
||||
if cv.UrgentSecurityUpdate {
|
||||
fmt.Fprintf(Stderr, "\nSecurity update available: %v -> %v\n", version.Short(), cv.LatestVersion)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nUpdate available: %v -> %v\n", version.Short(), cv.LatestVersion)
|
||||
}
|
||||
fmt.Fprintf(Stderr, "\nUpdate available: %v -> %v\n", version.Short(), cv.LatestVersion)
|
||||
fmt.Fprintln(Stderr, "Changelog: https://tailscale.com/changelog/#client")
|
||||
fmt.Fprintln(Stderr, "Run `tailscale update` or `tailscale set --auto-update` to update")
|
||||
}
|
||||
@@ -730,7 +725,6 @@ func init() {
|
||||
addPrefFlagMapping("nickname", "ProfileName")
|
||||
addPrefFlagMapping("update-check", "AutoUpdate")
|
||||
addPrefFlagMapping("auto-update", "AutoUpdate")
|
||||
addPrefFlagMapping("posture-checking", "PostureChecking")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
|
||||
@@ -20,12 +20,13 @@ import (
|
||||
var updateCmd = &ffcli.Command{
|
||||
Name: "update",
|
||||
ShortUsage: "update",
|
||||
ShortHelp: "[BETA] Update Tailscale to the latest/different version",
|
||||
ShortHelp: "[ALPHA] Update Tailscale to the latest/different version",
|
||||
Exec: runUpdate,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("update")
|
||||
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts")
|
||||
fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts")
|
||||
fs.BoolVar(&updateArgs.appStore, "app-store", false, "HIDDEN: check the App Store for updates, even if this is not an App Store install (for testing only)")
|
||||
// These flags are not supported on several systems that only provide
|
||||
// the latest version of Tailscale:
|
||||
//
|
||||
@@ -41,10 +42,11 @@ var updateCmd = &ffcli.Command{
|
||||
}
|
||||
|
||||
var updateArgs struct {
|
||||
yes bool
|
||||
dryRun bool
|
||||
track string // explicit track; empty means same as current
|
||||
version string // explicit version; empty means auto
|
||||
yes bool
|
||||
dryRun bool
|
||||
appStore bool
|
||||
track string // explicit track; empty means same as current
|
||||
version string // explicit version; empty means auto
|
||||
}
|
||||
|
||||
func runUpdate(ctx context.Context, args []string) error {
|
||||
@@ -59,11 +61,10 @@ func runUpdate(ctx context.Context, args []string) error {
|
||||
ver = updateArgs.track
|
||||
}
|
||||
err := clientupdate.Update(clientupdate.Arguments{
|
||||
Version: ver,
|
||||
Logf: func(f string, a ...any) { printf(f+"\n", a...) },
|
||||
Stdout: Stdout,
|
||||
Stderr: Stderr,
|
||||
Confirm: confirmUpdate,
|
||||
Version: ver,
|
||||
AppStore: updateArgs.appStore,
|
||||
Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
|
||||
Confirm: confirmUpdate,
|
||||
})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
return errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
|
||||
@@ -82,14 +83,7 @@ func confirmUpdate(ver string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("This will update Tailscale from %v to %v. Continue?", version.Short(), ver)
|
||||
return promptYesNo(msg)
|
||||
}
|
||||
|
||||
// PromptYesNo takes a question and prompts the user to answer the
|
||||
// question with a yes or no. It appends a [y/n] to the message.
|
||||
func promptYesNo(msg string) bool {
|
||||
fmt.Print(msg + " [y/n] ")
|
||||
fmt.Printf("This will update Tailscale from %v to %v. Continue? [y/n] ", version.Short(), ver)
|
||||
var resp string
|
||||
fmt.Scanln(&resp)
|
||||
resp = strings.ToLower(resp)
|
||||
|
||||
@@ -80,7 +80,7 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
webServer, cleanup := web.NewServer(web.ServerOpts{
|
||||
webServer, cleanup := web.NewServer(ctx, web.ServerOpts{
|
||||
DevMode: webArgs.dev,
|
||||
CGIMode: webArgs.cgi,
|
||||
PathPrefix: webArgs.prefix,
|
||||
|
||||
@@ -2,6 +2,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -12,6 +17,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
@@ -42,11 +48,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
@@ -64,6 +65,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/netipx from tailscale.com/wgengine/filter+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
@@ -116,7 +133,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
@@ -141,7 +158,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/views from tailscale.com/tailcfg+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -152,14 +169,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
|
||||
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/truncate from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
@@ -2,6 +2,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -81,7 +86,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
|
||||
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
|
||||
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
@@ -128,14 +132,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/pierrec/lz4/v4/internal/lz4errors from github.com/pierrec/lz4/v4+
|
||||
L github.com/pierrec/lz4/v4/internal/lz4stream from github.com/pierrec/lz4/v4
|
||||
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
|
||||
@@ -147,7 +147,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
@@ -239,7 +238,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
@@ -277,7 +275,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
@@ -295,14 +292,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/control/controlclient+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
@@ -333,7 +329,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
|
||||
@@ -341,17 +337,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/logpolicy+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
@@ -359,13 +354,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
|
||||
W tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag+
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
@@ -392,7 +386,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
|
||||
golang.org/x/crypto/hkdf from crypto/tls+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store"
|
||||
@@ -128,7 +127,6 @@ var args struct {
|
||||
tunname string
|
||||
|
||||
cleanup bool
|
||||
confFile string
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
@@ -174,7 +172,6 @@ func main() {
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
||||
flag.StringVar(&args.confFile, "config", "", "path to config file")
|
||||
|
||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
||||
beCLI()
|
||||
@@ -342,17 +339,6 @@ func run() error {
|
||||
|
||||
sys := new(tsd.System)
|
||||
|
||||
// Parse config, if specified, to fail early if it's invalid.
|
||||
var conf *conffile.Config
|
||||
if args.confFile != "" {
|
||||
var err error
|
||||
conf, err = conffile.Load(args.confFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
sys.InitialConfig = conf
|
||||
}
|
||||
|
||||
netMon, err := netmon.New(func(format string, args ...any) {
|
||||
logf(format, args...)
|
||||
})
|
||||
@@ -554,10 +540,6 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
}
|
||||
sys.Set(store)
|
||||
|
||||
if w, ok := sys.Tun.GetOK(); ok {
|
||||
w.Start()
|
||||
}
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// defaultTestArgs contains the default values for all flags in the testing
|
||||
// package. It is used to reset the flag values in testwrapper tests to allow
|
||||
// parsing the flags again.
|
||||
var defaultTestArgs map[string]string
|
||||
|
||||
// initDefaultTestArgs initializes defaultTestArgs.
|
||||
func initDefaultTestArgs() {
|
||||
if defaultTestArgs != nil {
|
||||
return
|
||||
}
|
||||
defaultTestArgs = make(map[string]string)
|
||||
flag.CommandLine.VisitAll(func(f *flag.Flag) {
|
||||
defaultTestArgs[f.Name] = f.DefValue
|
||||
})
|
||||
}
|
||||
|
||||
// registerTestFlags registers all flags from the testing package with the
|
||||
// provided flag set. It does so by calling testing.Init() and then iterating
|
||||
// over all flags registered on flag.CommandLine.
|
||||
func registerTestFlags(fs *flag.FlagSet) {
|
||||
testing.Init()
|
||||
type bv interface {
|
||||
IsBoolFlag() bool
|
||||
}
|
||||
|
||||
flag.CommandLine.VisitAll(func(f *flag.Flag) {
|
||||
if b, ok := f.Value.(bv); ok && b.IsBoolFlag() {
|
||||
fs.Bool(f.Name, f.DefValue == "true", f.Usage)
|
||||
if name, ok := strings.CutPrefix(f.Name, "test."); ok {
|
||||
fs.Bool(name, f.DefValue == "true", f.Usage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// We don't actually care about the value of the flag, so we just
|
||||
// register it as a string. The values will be passed to `go test` which
|
||||
// will parse and validate them anyway.
|
||||
fs.String(f.Name, f.DefValue, f.Usage)
|
||||
if name, ok := strings.CutPrefix(f.Name, "test."); ok {
|
||||
fs.String(name, f.DefValue, f.Usage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// splitArgs splits args into three parts as consumed by go test.
|
||||
//
|
||||
// go test [build/test flags] [packages] [build/test flags & test binary flags]
|
||||
//
|
||||
// We return these as three slices of strings [pre] [pkgs] [post].
|
||||
//
|
||||
// It is used to split the arguments passed to testwrapper into the arguments
|
||||
// passed to go test and the arguments passed to the tests.
|
||||
func splitArgs(args []string) (pre, pkgs, post []string, _ error) {
|
||||
if len(args) == 0 {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
|
||||
fs := newTestFlagSet()
|
||||
// Parse stops at the first non-flag argument, so this allows us
|
||||
// to parse those as values and then reconstruct them as args.
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
if f.Value.String() != f.DefValue && f.DefValue != "false" {
|
||||
pre = append(pre, "-"+f.Name, f.Value.String())
|
||||
} else {
|
||||
pre = append(pre, "-"+f.Name)
|
||||
}
|
||||
})
|
||||
|
||||
// fs.Args() now contains [packages]+[build/test flags & test binary flags],
|
||||
// to split it we need to find the first non-flag argument.
|
||||
rem := fs.Args()
|
||||
ix := slices.IndexFunc(rem, func(s string) bool { return strings.HasPrefix(s, "-") })
|
||||
if ix == -1 {
|
||||
return pre, rem, nil, nil
|
||||
}
|
||||
pkgs = rem[:ix]
|
||||
post = rem[ix:]
|
||||
return pre, pkgs, post, nil
|
||||
}
|
||||
|
||||
func newTestFlagSet() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("testwrapper", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
// Register all flags from the testing package.
|
||||
registerTestFlags(fs)
|
||||
// Also register the -exec flag, which is not part of the testing package.
|
||||
// TODO(maisem): figure out what other flags we need to register explicitly.
|
||||
fs.String("exec", "", "Command to run tests with")
|
||||
fs.Bool("race", false, "build with race detector")
|
||||
return fs
|
||||
}
|
||||
|
||||
// testingVerbose reports whether the test is being run with verbose logging.
|
||||
var testingVerbose = func() bool {
|
||||
verbose := false
|
||||
|
||||
// Likely doesn't matter, but to be correct follow the go flag parsing logic
|
||||
// of overriding previous values.
|
||||
for _, arg := range os.Args[1:] {
|
||||
switch arg {
|
||||
case "-test.v", "--test.v",
|
||||
"-test.v=true", "--test.v=true",
|
||||
"-v", "--v",
|
||||
"-v=true", "--v=true":
|
||||
verbose = true
|
||||
case "-test.v=false", "--test.v=false",
|
||||
"-v=false", "--v=false":
|
||||
verbose = false
|
||||
}
|
||||
}
|
||||
return verbose
|
||||
}()
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
pre, pkgs, post []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "all",
|
||||
in: []string{"-v", "pkg1", "pkg2", "-run", "TestFoo", "-timeout=20s"},
|
||||
pre: []string{"-v"},
|
||||
pkgs: []string{"pkg1", "pkg2"},
|
||||
post: []string{"-run", "TestFoo", "-timeout=20s"},
|
||||
},
|
||||
{
|
||||
name: "only_pkgs",
|
||||
in: []string{"./..."},
|
||||
pkgs: []string{"./..."},
|
||||
},
|
||||
{
|
||||
name: "pkgs_and_post",
|
||||
in: []string{"pkg1", "-run", "TestFoo"},
|
||||
pkgs: []string{"pkg1"},
|
||||
post: []string{"-run", "TestFoo"},
|
||||
},
|
||||
{
|
||||
name: "pkgs_and_post",
|
||||
in: []string{"-v", "pkg2"},
|
||||
pre: []string{"-v"},
|
||||
pkgs: []string{"pkg2"},
|
||||
},
|
||||
{
|
||||
name: "only_args",
|
||||
in: []string{"-v", "-run=TestFoo"},
|
||||
pre: []string{"-run", "TestFoo", "-v"}, // sorted
|
||||
},
|
||||
{
|
||||
name: "space_in_pre_arg",
|
||||
in: []string{"-run", "TestFoo", "./cmd/testwrapper"},
|
||||
pre: []string{"-run", "TestFoo"},
|
||||
pkgs: []string{"./cmd/testwrapper"},
|
||||
},
|
||||
{
|
||||
name: "space_in_arg",
|
||||
in: []string{"-exec", "sudo -E", "./cmd/testwrapper"},
|
||||
pre: []string{"-exec", "sudo -E"},
|
||||
pkgs: []string{"./cmd/testwrapper"},
|
||||
},
|
||||
{
|
||||
name: "test-arg",
|
||||
in: []string{"-exec", "sudo -E", "./cmd/testwrapper", "--", "--some-flag"},
|
||||
pre: []string{"-exec", "sudo -E"},
|
||||
pkgs: []string{"./cmd/testwrapper"},
|
||||
post: []string{"--", "--some-flag"},
|
||||
},
|
||||
{
|
||||
name: "dupe-args",
|
||||
in: []string{"-v", "-v", "-race", "-race", "./cmd/testwrapper", "--", "--some-flag"},
|
||||
pre: []string{"-race", "-v"},
|
||||
pkgs: []string{"./cmd/testwrapper"},
|
||||
post: []string{"--", "--some-flag"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pre, pkgs, post, err := splitArgs(tt.in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(pre, tt.pre) {
|
||||
t.Errorf("pre = %q; want %q", pre, tt.pre)
|
||||
}
|
||||
if !slices.Equal(pkgs, tt.pkgs) {
|
||||
t.Errorf("pattern = %q; want %q", pkgs, tt.pkgs)
|
||||
}
|
||||
if !slices.Equal(post, tt.post) {
|
||||
t.Errorf("post = %q; want %q", post, tt.post)
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Logf("SplitArgs(%q) = %q %q %q", tt.in, pre, pkgs, post)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -70,24 +71,18 @@ var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||
// set to true. Package build errors will not emit a testAttempt (as no valid
|
||||
// JSON is produced) but the [os/exec.ExitError] will be returned.
|
||||
// It calls close(ch) when it's done.
|
||||
func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, testArgs []string, ch chan<- *testAttempt) error {
|
||||
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) error {
|
||||
defer close(ch)
|
||||
args := []string{"test"}
|
||||
args = append(args, goTestArgs...)
|
||||
args = append(args, pt.Pattern)
|
||||
args := []string{"test", "-json", pt.Pattern}
|
||||
args = append(args, otherArgs...)
|
||||
if len(pt.Tests) > 0 {
|
||||
runArg := strings.Join(pt.Tests, "|")
|
||||
args = append(args, "--run", runArg)
|
||||
args = append(args, "-run", runArg)
|
||||
}
|
||||
args = append(args, testArgs...)
|
||||
args = append(args, "-json")
|
||||
if debug {
|
||||
fmt.Println("running", strings.Join(args, " "))
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "go", args...)
|
||||
if len(pt.Tests) > 0 {
|
||||
cmd.Env = append(os.Environ(), "TS_TEST_SHARD=") // clear test shard; run all tests we say to run
|
||||
}
|
||||
r, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("error creating stdout pipe: %v", err)
|
||||
@@ -183,28 +178,54 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
|
||||
}
|
||||
|
||||
func main() {
|
||||
goTestArgs, packages, testArgs, err := splitArgs(os.Args[1:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
if len(packages) == 0 {
|
||||
fmt.Println("testwrapper: no packages specified")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// We only need to parse the -v flag to figure out whether to print the logs
|
||||
// for a test. We don't need to parse any other flags, so we just use the
|
||||
// flag package to parse the -v flag and then pass the rest of the args
|
||||
// through to 'go test'.
|
||||
// We run `go test -json` which returns the same information as `go test -v`,
|
||||
// but in a machine-readable format. So this flag is only for testwrapper's
|
||||
// output.
|
||||
v := flag.Bool("v", false, "verbose")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
|
||||
fmt.Println()
|
||||
fmt.Println("testwrapper-flags:")
|
||||
flag.CommandLine.PrintDefaults()
|
||||
fmt.Println()
|
||||
fmt.Println("examples:")
|
||||
fmt.Println("\ttestwrapper -v ./... -count=1")
|
||||
fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
|
||||
fmt.Println()
|
||||
fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) < 1 || strings.HasPrefix(args[0], "-") {
|
||||
fmt.Println("no pattern specified")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
} else if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
|
||||
fmt.Println("expected single pattern")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
pattern, otherArgs := args[0], args[1:]
|
||||
|
||||
type nextRun struct {
|
||||
tests []*packageTests
|
||||
attempt int // starting at 1
|
||||
}
|
||||
firstRun := &nextRun{
|
||||
attempt: 1,
|
||||
|
||||
toRun := []*nextRun{
|
||||
{
|
||||
tests: []*packageTests{{Pattern: pattern}},
|
||||
attempt: 1,
|
||||
},
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
firstRun.tests = append(firstRun.tests, &packageTests{Pattern: pkg})
|
||||
}
|
||||
toRun := []*nextRun{firstRun}
|
||||
printPkgOutcome := func(pkg, outcome string, attempt int) {
|
||||
if outcome == "skip" {
|
||||
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
|
||||
@@ -242,7 +263,7 @@ func main() {
|
||||
runErr := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(runErr)
|
||||
runErr <- runTests(ctx, thisRun.attempt, pt, goTestArgs, testArgs, ch)
|
||||
runErr <- runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
|
||||
}()
|
||||
|
||||
var failed bool
|
||||
@@ -252,7 +273,7 @@ func main() {
|
||||
// convenient for us to to specify files in tests, so fix tr.pkg
|
||||
// so that subsequent testwrapper attempts run correctly.
|
||||
if tr.pkg == "command-line-arguments" {
|
||||
tr.pkg = packages[0]
|
||||
tr.pkg = pattern
|
||||
}
|
||||
if tr.pkgFinished {
|
||||
if tr.outcome == "fail" && len(toRetry[tr.pkg]) == 0 {
|
||||
@@ -264,7 +285,7 @@ func main() {
|
||||
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt)
|
||||
continue
|
||||
}
|
||||
if testingVerbose || tr.outcome == "fail" {
|
||||
if *v || tr.outcome == "fail" {
|
||||
io.Copy(os.Stdout, &tr.logs)
|
||||
}
|
||||
if tr.outcome != "fail" {
|
||||
|
||||
@@ -125,7 +125,6 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
sys.NetstackRouter.Set(true)
|
||||
sys.Tun.Get().Start()
|
||||
|
||||
logid := lpc.PublicID
|
||||
srv := ipnserver.New(logf, logid, sys.NetMon.Get())
|
||||
|
||||
@@ -55,7 +55,6 @@ import (
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/systemd"
|
||||
)
|
||||
|
||||
@@ -567,11 +566,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
|
||||
tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
|
||||
if err != nil {
|
||||
c.logf("unable to provide Tailnet field in register request. err: %v", err)
|
||||
}
|
||||
now := c.clock.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
@@ -583,7 +577,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
NodeKeySignature: nodeKeySignature,
|
||||
Tailnet: tailnet,
|
||||
}
|
||||
if opt.Logout {
|
||||
request.Expiry = time.Unix(123, 0) // far in the past
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build windows
|
||||
//go:build windows && cgo
|
||||
|
||||
// darwin,cgo is also supported by certstore but untested, so it is not enabled.
|
||||
// darwin,cgo is also supported by certstore but machineCertificateSubject will
|
||||
// need to be loaded by a different mechanism, so this is not currently enabled
|
||||
// on darwin.
|
||||
|
||||
package controlclient
|
||||
|
||||
@@ -19,7 +21,7 @@ import (
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
var getMachineCertificateSubjectOnce struct {
|
||||
@@ -38,7 +40,7 @@ var getMachineCertificateSubjectOnce struct {
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
func getMachineCertificateSubject() string {
|
||||
getMachineCertificateSubjectOnce.Do(func() {
|
||||
getMachineCertificateSubjectOnce.v, _ = syspolicy.GetString("MachineCertificateSubject", "")
|
||||
getMachineCertificateSubjectOnce.v, _ = winutil.GetRegString("MachineCertificateSubject")
|
||||
})
|
||||
|
||||
return getMachineCertificateSubjectOnce.v
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
//go:build !windows || !cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ package controlknobs
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -52,6 +54,10 @@ type Knobs struct {
|
||||
// DisableDNSForwarderTCPRetries is whether the DNS forwarder should
|
||||
// skip retrying truncated queries over TCP.
|
||||
DisableDNSForwarderTCPRetries atomic.Bool
|
||||
|
||||
// MagicsockSessionActiveTimeout is an alternate magicsock session timeout
|
||||
// duration to use. If zero or unset, the default is used.
|
||||
MagicsockSessionActiveTimeout syncs.AtomicValue[time.Duration]
|
||||
}
|
||||
|
||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||
@@ -91,6 +97,17 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability,
|
||||
k.DisableDeltaUpdates.Store(disableDeltaUpdates)
|
||||
k.PeerMTUEnable.Store(peerMTUEnable)
|
||||
k.DisableDNSForwarderTCPRetries.Store(dnsForwarderDisableTCPRetries)
|
||||
|
||||
var timeout time.Duration
|
||||
if vv := capMap[tailcfg.NodeAttrMagicsockSessionTimeout]; len(vv) > 0 {
|
||||
if v, _ := strconv.Unquote(string(vv[0])); v != "" {
|
||||
timeout, _ = time.ParseDuration(v)
|
||||
timeout = max(timeout, 0)
|
||||
}
|
||||
}
|
||||
if was := k.MagicsockSessionActiveTimeout.Load(); was != timeout {
|
||||
k.MagicsockSessionActiveTimeout.Store(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||
@@ -109,5 +126,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
"DisableDeltaUpdates": k.DisableDeltaUpdates.Load(),
|
||||
"PeerMTUEnable": k.PeerMTUEnable.Load(),
|
||||
"DisableDNSForwarderTCPRetries": k.DisableDNSForwarderTCPRetries.Load(),
|
||||
"MagicsockSessionActiveTimeout": k.MagicsockSessionActiveTimeout.Load().String(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func (c *Client) useHTTPS() bool {
|
||||
// tlsServerName returns the tls.Config.ServerName value (for the TLS ClientHello).
|
||||
func (c *Client) tlsServerName(node *tailcfg.DERPNode) string {
|
||||
if c.url != nil {
|
||||
return c.url.Hostname()
|
||||
return c.url.Host
|
||||
}
|
||||
return node.HostName
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
|
||||
func MessageSummary(m Message) string {
|
||||
switch m := m.(type) {
|
||||
case *Ping:
|
||||
return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case *CallMeMaybe:
|
||||
|
||||
@@ -11,7 +11,7 @@ spec:
|
||||
# the container. The `net.ipv4.ip_forward` sysctl is not allowlisted
|
||||
# in Kubelet by default.
|
||||
- name: sysctler
|
||||
image: "ghcr.io/tailscale/tailscale:latest"
|
||||
image: busybox
|
||||
securityContext:
|
||||
privileged: true
|
||||
command: ["/bin/sh"]
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: tailscaled
|
||||
# Required-Start:
|
||||
# Required-Stop:
|
||||
# Default-Start:
|
||||
# Default-Stop:
|
||||
# Short-Description: Tailscale Mesh Wireguard VPN
|
||||
### END INIT INFO
|
||||
|
||||
set -e
|
||||
|
||||
# /etc/init.d/tailscale: start and stop the Tailscale VPN service
|
||||
|
||||
test -x /usr/sbin/tailscaled || exit 0
|
||||
|
||||
umask 022
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
# Are we running from init?
|
||||
run_by_init() {
|
||||
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
|
||||
}
|
||||
|
||||
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
log_daemon_msg "Starting Tailscale VPN" "tailscaled" || true
|
||||
if start-stop-daemon --start --oknodo --name tailscaled -m --pidfile /run/tailscaled.pid --background \
|
||||
--exec /usr/sbin/tailscaled -- \
|
||||
--state=/var/lib/tailscale/tailscaled.state \
|
||||
--socket=/run/tailscale/tailscaled.sock \
|
||||
--port 41641;
|
||||
then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping Tailscale VPN" "tailscaled" || true
|
||||
if start-stop-daemon --stop --remove-pidfile --pidfile /run/tailscaled.pid --exec /usr/sbin/tailscaled; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
status_of_proc -p /run/tailscaled.pid /usr/sbin/tailscaled tailscaled && exit 0 || exit $?
|
||||
;;
|
||||
|
||||
*)
|
||||
log_action_msg "Usage: /etc/init.d/tailscaled {start|stop|status}" || true
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
29
flake.nix
29
flake.nix
@@ -41,6 +41,14 @@
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, flake-compat }: let
|
||||
# Grab a helper func out of the Nix language libraries. Annoyingly
|
||||
# these are only accessible through legacyPackages right now,
|
||||
# which forces us to indirect through a platform-specific
|
||||
# path. The x86_64-linux in here doesn't really matter, since all
|
||||
# we're grabbing is a pure Nix string manipulation function that
|
||||
# doesn't build any software.
|
||||
fileContents = nixpkgs.legacyPackages.x86_64-linux.lib.fileContents;
|
||||
|
||||
# tailscaleRev is the git commit at which this flake was imported,
|
||||
# or the empty string when building from a local checkout of the
|
||||
# tailscale repo.
|
||||
@@ -66,29 +74,17 @@
|
||||
name = "tailscale";
|
||||
|
||||
src = ./.;
|
||||
vendorSha256 = pkgs.lib.fileContents ./go.mod.sri;
|
||||
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
|
||||
vendorSha256 = fileContents ./go.mod.sri;
|
||||
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper pkgs.git ];
|
||||
ldflags = ["-X tailscale.com/version.GitCommit=${tailscaleRev}"];
|
||||
CGO_ENABLED = 0;
|
||||
subPackages = [ "cmd/tailscale" "cmd/tailscaled" ];
|
||||
doCheck = false;
|
||||
|
||||
# NOTE: We strip the ${PORT} and $FLAGS because they are unset in the
|
||||
# environment and cause issues (specifically the unset PORT). At some
|
||||
# point, there should be a NixOS module that allows configuration of these
|
||||
# things, but for now, we hardcode the default of port 41641 (taken from
|
||||
# ./cmd/tailscaled/tailscaled.defaults).
|
||||
postInstall = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
||||
wrapProgram $out/bin/tailscaled --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.iproute2 pkgs.iptables pkgs.getent pkgs.shadow ]}
|
||||
wrapProgram $out/bin/tailscale --suffix PATH : ${pkgs.lib.makeBinPath [ pkgs.procps ]}
|
||||
|
||||
sed -i \
|
||||
-e "s#/usr/sbin#$out/bin#" \
|
||||
-e "/^EnvironmentFile/d" \
|
||||
-e 's/''${PORT}/41641/' \
|
||||
-e 's/$FLAGS//' \
|
||||
./cmd/tailscaled/tailscaled.service
|
||||
|
||||
sed -i -e "s#/usr/sbin#$out/bin#" -e "/^EnvironmentFile/d" ./cmd/tailscaled/tailscaled.service
|
||||
install -D -m0444 -t $out/lib/systemd/system ./cmd/tailscaled/tailscaled.service
|
||||
'';
|
||||
};
|
||||
@@ -101,7 +97,6 @@
|
||||
ts = tailscale pkgs;
|
||||
in {
|
||||
packages = {
|
||||
default = ts;
|
||||
tailscale = ts;
|
||||
};
|
||||
devShell = pkgs.mkShell {
|
||||
@@ -120,4 +115,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-WGZkpffwe4I8FewdBHXGaLbKQP/kHr7UF2lCXBTcNb4=
|
||||
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
|
||||
16
go.mod
16
go.mod
@@ -4,8 +4,9 @@ go 1.21
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/andybalholm/brotli v1.0.5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aws/aws-sdk-go-v2 v1.21.0
|
||||
@@ -19,7 +20,6 @@ require (
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/dave/jennifer v1.7.0
|
||||
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||
github.com/dsnet/try v0.0.3
|
||||
github.com/evanw/esbuild v0.19.4
|
||||
github.com/frankban/quicktest v1.14.5
|
||||
@@ -57,7 +57,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/common v0.44.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff
|
||||
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-20230713185742-f0b76a10a08e
|
||||
@@ -76,14 +76,14 @@ require (
|
||||
go.uber.org/zap v1.26.0
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/mod v0.12.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/net v0.15.0
|
||||
golang.org/x/oauth2 v0.12.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/sys v0.12.0
|
||||
golang.org/x/term v0.12.0
|
||||
golang.org/x/time v0.3.0
|
||||
golang.org/x/tools v0.13.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
@@ -103,7 +103,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
)
|
||||
@@ -321,7 +320,6 @@ require (
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55
|
||||
github.com/tdakkota/asciicheck v0.2.0 // indirect
|
||||
github.com/tetafro/godot v1.4.11 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-WGZkpffwe4I8FewdBHXGaLbKQP/kHr7UF2lCXBTcNb4=
|
||||
sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
|
||||
28
go.sum
28
go.sum
@@ -93,8 +93,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
|
||||
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
|
||||
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
|
||||
@@ -233,8 +233,6 @@ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0
|
||||
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
|
||||
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
|
||||
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
|
||||
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
@@ -864,12 +862,10 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8=
|
||||
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk=
|
||||
github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff h1:vnxdYZUJbsSRcIcduDW3DcQqfqaiv4FUgy25q8X+vfI=
|
||||
github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8=
|
||||
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE=
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
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-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ=
|
||||
@@ -988,8 +984,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
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=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1084,8 +1080,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1181,8 +1177,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -1191,8 +1187,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -1 +1 @@
|
||||
56d25cd9a2efe0eee3945518fc532ec45912ccb2
|
||||
f242beecd311476f6e6b9fa3052e253e2301e170
|
||||
|
||||
@@ -53,6 +53,7 @@ func New() *tailcfg.Hostinfo {
|
||||
GoVersion: runtime.Version(),
|
||||
Machine: condCall(unameMachine),
|
||||
DeviceModel: deviceModel(),
|
||||
PushDeviceToken: pushDeviceToken(),
|
||||
Cloud: string(cloudenv.Get()),
|
||||
NoLogsNoSupport: envknob.NoLogsNoSupport(),
|
||||
AllowsUpdate: envknob.AllowsRemoteUpdate(),
|
||||
@@ -165,14 +166,18 @@ func GetEnvType() EnvType {
|
||||
}
|
||||
|
||||
var (
|
||||
deviceModelAtomic atomic.Value // of string
|
||||
osVersionAtomic atomic.Value // of string
|
||||
desktopAtomic atomic.Value // of opt.Bool
|
||||
packagingType atomic.Value // of string
|
||||
appType atomic.Value // of string
|
||||
firewallMode atomic.Value // of string
|
||||
pushDeviceTokenAtomic atomic.Value // of string
|
||||
deviceModelAtomic atomic.Value // of string
|
||||
osVersionAtomic atomic.Value // of string
|
||||
desktopAtomic atomic.Value // of opt.Bool
|
||||
packagingType atomic.Value // of string
|
||||
appType atomic.Value // of string
|
||||
firewallMode atomic.Value // of string
|
||||
)
|
||||
|
||||
// SetPushDeviceToken sets the device token for use in Hostinfo updates.
|
||||
func SetPushDeviceToken(token string) { pushDeviceTokenAtomic.Store(token) }
|
||||
|
||||
// SetDeviceModel sets the device model for use in Hostinfo updates.
|
||||
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
|
||||
|
||||
@@ -198,6 +203,11 @@ func deviceModel() string {
|
||||
return s
|
||||
}
|
||||
|
||||
func pushDeviceToken() string {
|
||||
s, _ := pushDeviceTokenAtomic.Load().(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// FirewallMode returns the firewall mode for the app.
|
||||
// It is empty if unset.
|
||||
func FirewallMode() string {
|
||||
|
||||
125
ipn/conf.go
125
ipn/conf.go
@@ -1,125 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// ConfigVAlpha is the config file format for the "alpha0" version.
|
||||
type ConfigVAlpha struct {
|
||||
Version string // "alpha0" for now
|
||||
Locked opt.Bool `json:",omitempty"` // whether the config is locked from being changed by 'tailscale set'; it defaults to true
|
||||
|
||||
ServerURL *string `json:",omitempty"` // defaults to https://controlplane.tailscale.com
|
||||
AuthKey *string `json:",omitempty"` // as needed if NeedsLogin. either key or path to a file (if prefixed with "file:")
|
||||
Enabled opt.Bool `json:",omitempty"` // wantRunning; empty string defaults to true
|
||||
|
||||
OperatorUser *string `json:",omitempty"` // local user name who is allowed to operate tailscaled without being root or using sudo
|
||||
Hostname *string `json:",omitempty"`
|
||||
|
||||
AcceptDNS opt.Bool `json:"acceptDNS,omitempty"` // --accept-dns
|
||||
AcceptRoutes opt.Bool `json:"acceptRoutes,omitempty"`
|
||||
|
||||
ExitNode *string `json:"exitNode,omitempty"` // IP, StableID, or MagicDNS base name
|
||||
AllowLANWhileUsingExitNode opt.Bool `json:"allowLANWhileUsingExitNode,omitempty"`
|
||||
|
||||
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
|
||||
DisableSNAT opt.Bool `json:",omitempty"`
|
||||
|
||||
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
|
||||
|
||||
PostureChecking opt.Bool `json:",omitempty"`
|
||||
RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH
|
||||
ShieldsUp opt.Bool `json:",omitempty"`
|
||||
AutoUpdate *AutoUpdatePrefs `json:",omitempty"`
|
||||
ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this
|
||||
|
||||
// TODO(bradfitz,maisem): future something like:
|
||||
// Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID)
|
||||
}
|
||||
|
||||
func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
|
||||
var mp MaskedPrefs
|
||||
if c == nil {
|
||||
return mp, nil
|
||||
}
|
||||
mp.WantRunning = !c.Enabled.EqualBool(false)
|
||||
mp.WantRunningSet = mp.WantRunning || c.Enabled != ""
|
||||
if c.ServerURL != nil {
|
||||
mp.ControlURL = *c.ServerURL
|
||||
mp.ControlURLSet = true
|
||||
}
|
||||
if c.AuthKey != nil && *c.AuthKey != "" {
|
||||
mp.LoggedOut = false
|
||||
mp.LoggedOutSet = true
|
||||
}
|
||||
if c.OperatorUser != nil {
|
||||
mp.OperatorUser = *c.OperatorUser
|
||||
mp.OperatorUserSet = true
|
||||
}
|
||||
if c.Hostname != nil {
|
||||
mp.Hostname = *c.Hostname
|
||||
mp.HostnameSet = true
|
||||
}
|
||||
if c.AcceptDNS != "" {
|
||||
mp.CorpDNS = c.AcceptDNS.EqualBool(true)
|
||||
mp.CorpDNSSet = true
|
||||
}
|
||||
if c.AcceptRoutes != "" {
|
||||
mp.RouteAll = c.AcceptRoutes.EqualBool(true)
|
||||
mp.RouteAllSet = true
|
||||
}
|
||||
if c.ExitNode != nil {
|
||||
ip, err := netip.ParseAddr(*c.ExitNode)
|
||||
if err == nil {
|
||||
mp.ExitNodeIP = ip
|
||||
mp.ExitNodeIPSet = true
|
||||
} else {
|
||||
mp.ExitNodeID = tailcfg.StableNodeID(*c.ExitNode)
|
||||
mp.ExitNodeIDSet = true
|
||||
}
|
||||
}
|
||||
if c.AllowLANWhileUsingExitNode != "" {
|
||||
mp.ExitNodeAllowLANAccess = c.AllowLANWhileUsingExitNode.EqualBool(true)
|
||||
mp.ExitNodeAllowLANAccessSet = true
|
||||
}
|
||||
if c.AdvertiseRoutes != nil {
|
||||
mp.AdvertiseRoutes = c.AdvertiseRoutes
|
||||
mp.AdvertiseRoutesSet = true
|
||||
}
|
||||
if c.DisableSNAT != "" {
|
||||
mp.NoSNAT = c.DisableSNAT.EqualBool(true)
|
||||
mp.NoSNAT = true
|
||||
}
|
||||
if c.NetfilterMode != nil {
|
||||
m, err := preftype.ParseNetfilterMode(*c.NetfilterMode)
|
||||
if err != nil {
|
||||
return mp, err
|
||||
}
|
||||
mp.NetfilterMode = m
|
||||
mp.NetfilterModeSet = true
|
||||
}
|
||||
if c.PostureChecking != "" {
|
||||
mp.PostureChecking = c.PostureChecking.EqualBool(true)
|
||||
mp.PostureCheckingSet = true
|
||||
}
|
||||
if c.RunSSHServer != "" {
|
||||
mp.RunSSH = c.RunSSHServer.EqualBool(true)
|
||||
mp.RunSSHSet = true
|
||||
}
|
||||
if c.ShieldsUp != "" {
|
||||
mp.ShieldsUp = c.ShieldsUp.EqualBool(true)
|
||||
mp.ShieldsUpSet = true
|
||||
}
|
||||
if c.AutoUpdate != nil {
|
||||
mp.AutoUpdate = *c.AutoUpdate
|
||||
mp.AutoUpdateSet = true
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package conffile contains code to load, manipulate, and access config file
|
||||
// settings.
|
||||
package conffile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
// Config describes a config file.
|
||||
type Config struct {
|
||||
Path string // disk path of HuJSON
|
||||
Raw []byte // raw bytes from disk, in HuJSON form
|
||||
Std []byte // standardized JSON form
|
||||
Version string // "alpha0" for now
|
||||
|
||||
// Parsed is the parsed config, converted from its on-disk version to the
|
||||
// latest known format.
|
||||
//
|
||||
// As of 2023-10-15 there is exactly one format ("alpha0") so this is both
|
||||
// the on-disk format and the in-memory upgraded format.
|
||||
Parsed ipn.ConfigVAlpha
|
||||
}
|
||||
|
||||
// WantRunning reports whether c is non-nil and it's configured to be running.
|
||||
func (c *Config) WantRunning() bool {
|
||||
return c != nil && !c.Parsed.Enabled.EqualBool(false)
|
||||
}
|
||||
|
||||
// Load reads and parses the config file at the provided path on disk.
|
||||
func Load(path string) (*Config, error) {
|
||||
var c Config
|
||||
c.Path = path
|
||||
|
||||
var err error
|
||||
c.Raw, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Std, err = hujson.Standardize(c.Raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file %s HuJSON/JSON: %w", path, err)
|
||||
}
|
||||
var ver struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Std, &ver); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
|
||||
}
|
||||
switch ver.Version {
|
||||
case "":
|
||||
return nil, fmt.Errorf("error parsing config file %s: no \"version\" field defined", path)
|
||||
case "alpha0":
|
||||
default:
|
||||
return nil, fmt.Errorf("error parsing config file %s: unsupported \"version\" value %q; want \"alpha0\" for now", path, ver.Version)
|
||||
}
|
||||
c.Version = ver.Version
|
||||
|
||||
jd := json.NewDecoder(bytes.NewReader(c.Std))
|
||||
jd.DisallowUnknownFields()
|
||||
err = jd.Decode(&c.Parsed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
|
||||
}
|
||||
if jd.More() {
|
||||
return nil, fmt.Errorf("error parsing config file %s: trailing data after JSON object", path)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
@@ -52,7 +52,6 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
PostureChecking bool
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
@@ -87,7 +87,6 @@ func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.Netfilte
|
||||
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
||||
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
|
||||
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
|
||||
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
@@ -114,7 +113,6 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
PostureChecking bool
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -27,35 +25,6 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not
|
||||
// implemented for the current GOOS.
|
||||
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||
|
||||
// WindowsToken represents the current security context of a Windows user.
|
||||
type WindowsToken interface {
|
||||
io.Closer
|
||||
// EqualUIDs reports whether other refers to the same user ID as the receiver.
|
||||
EqualUIDs(other WindowsToken) bool
|
||||
// IsAdministrator reports whether the receiver is a member of the built-in
|
||||
// Administrators group, or else an error. Use IsElevated to determine whether
|
||||
// the receiver is actually utilizing administrative rights.
|
||||
IsAdministrator() (bool, error)
|
||||
// IsUID reports whether the receiver's user ID matches uid.
|
||||
IsUID(uid ipn.WindowsUserID) bool
|
||||
// UID returns the ipn.WindowsUserID associated with the receiver, or else
|
||||
// an error.
|
||||
UID() (ipn.WindowsUserID, error)
|
||||
// IsElevated reports whether the receiver is currently executing as an
|
||||
// elevated administrative user.
|
||||
IsElevated() bool
|
||||
// UserDir returns the special directory identified by folderID as associated
|
||||
// with the receiver. folderID must be one of the KNOWNFOLDERID values from
|
||||
// the x/sys/windows package, serialized as a stringified GUID.
|
||||
UserDir(folderID string) (string, error)
|
||||
// Username returns the user name associated with the receiver.
|
||||
Username() (string, error)
|
||||
}
|
||||
|
||||
// ConnIdentity represents the owner of a localhost TCP or unix socket connection
|
||||
// connecting to the LocalAPI.
|
||||
type ConnIdentity struct {
|
||||
@@ -69,7 +38,9 @@ type ConnIdentity struct {
|
||||
// Used on Windows:
|
||||
// TODO(bradfitz): merge these into the peercreds package and
|
||||
// use that for all.
|
||||
pid int
|
||||
pid int
|
||||
userID ipn.WindowsUserID
|
||||
user *user.User
|
||||
}
|
||||
|
||||
// WindowsUserID returns the local machine's userid of the connection
|
||||
@@ -81,11 +52,8 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
||||
if envknob.GOOS() != "windows" {
|
||||
return ""
|
||||
}
|
||||
if tok, err := ci.WindowsToken(); err == nil {
|
||||
defer tok.Close()
|
||||
if uid, err := tok.UID(); err == nil {
|
||||
return uid
|
||||
}
|
||||
if ci.userID != "" {
|
||||
return ci.userID
|
||||
}
|
||||
// For Linux tests running as Windows:
|
||||
const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet
|
||||
@@ -97,6 +65,7 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ci *ConnIdentity) User() *user.User { return ci.user }
|
||||
func (ci *ConnIdentity) Pid() int { return ci.pid }
|
||||
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
|
||||
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
|
||||
|
||||
@@ -21,9 +21,3 @@ func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci.creds, _ = peercred.Get(c)
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
// WindowsToken is unsupported when GOOS != windows and always returns
|
||||
// ErrNotImplemented.
|
||||
func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
@@ -6,157 +6,53 @@ package ipnauth
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetNamedPipeClientProcessId = kernel32.NewProc("GetNamedPipeClientProcessId")
|
||||
)
|
||||
|
||||
func getNamedPipeClientProcessId(h windows.Handle) (pid uint32, err error) {
|
||||
r1, _, err := procGetNamedPipeClientProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid)))
|
||||
if r1 > 0 {
|
||||
return pid, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// GetConnIdentity extracts the identity information from the connection
|
||||
// based on the user who owns the other end of the connection.
|
||||
// If c is not backed by a named pipe, an error is returned.
|
||||
func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci = &ConnIdentity{conn: c}
|
||||
wcc, ok := c.(*safesocket.WindowsClientConn)
|
||||
h, ok := c.(interface {
|
||||
Fd() uintptr
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a WindowsClientConn: %T", c)
|
||||
return ci, fmt.Errorf("not a windows handle: %T", c)
|
||||
}
|
||||
ci.pid, err = wcc.ClientPID()
|
||||
pid, err := getNamedPipeClientProcessId(windows.Handle(h.Fd()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return ci, fmt.Errorf("getNamedPipeClientProcessId: %v", err)
|
||||
}
|
||||
ci.pid = int(pid)
|
||||
uid, err := pidowner.OwnerOfPID(ci.pid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to map connection's pid to a user (WSL?): %w", err)
|
||||
}
|
||||
ci.userID = ipn.WindowsUserID(uid)
|
||||
u, err := LookupUserFromID(logf, uid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||
}
|
||||
ci.user = u
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
type token struct {
|
||||
t windows.Token
|
||||
}
|
||||
|
||||
func (t *token) UID() (ipn.WindowsUserID, error) {
|
||||
sid, err := t.uid()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up user from token: %w", err)
|
||||
}
|
||||
|
||||
return ipn.WindowsUserID(sid.String()), nil
|
||||
}
|
||||
|
||||
func (t *token) Username() (string, error) {
|
||||
sid, err := t.uid()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up user from token: %w", err)
|
||||
}
|
||||
|
||||
username, domain, _, err := sid.LookupAccount("")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up username from SID: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`%s\%s`, domain, username), nil
|
||||
}
|
||||
|
||||
func (t *token) IsAdministrator() (bool, error) {
|
||||
baSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return t.t.IsMember(baSID)
|
||||
}
|
||||
|
||||
func (t *token) IsElevated() bool {
|
||||
return t.t.IsElevated()
|
||||
}
|
||||
|
||||
func (t *token) UserDir(folderID string) (string, error) {
|
||||
guid, err := windows.GUIDFromString(folderID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return t.t.KnownFolderPath((*windows.KNOWNFOLDERID)(unsafe.Pointer(&guid)), 0)
|
||||
}
|
||||
|
||||
func (t *token) Close() error {
|
||||
if t.t == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := t.t.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
t.t = 0
|
||||
runtime.SetFinalizer(t, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *token) EqualUIDs(other WindowsToken) bool {
|
||||
if t != nil && other == nil || t == nil && other != nil {
|
||||
return false
|
||||
}
|
||||
ot, ok := other.(*token)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if t == ot {
|
||||
return true
|
||||
}
|
||||
uid, err := t.uid()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
oUID, err := ot.uid()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return uid.Equals(oUID)
|
||||
}
|
||||
|
||||
func (t *token) uid() (*windows.SID, error) {
|
||||
tu, err := t.t.GetTokenUser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tu.User.Sid, nil
|
||||
}
|
||||
|
||||
func (t *token) IsUID(uid ipn.WindowsUserID) bool {
|
||||
tUID, err := t.UID()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return tUID == uid
|
||||
}
|
||||
|
||||
// WindowsToken returns the WindowsToken representing the security context
|
||||
// of the connection's client.
|
||||
func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
|
||||
var wcc *safesocket.WindowsClientConn
|
||||
var ok bool
|
||||
if wcc, ok = ci.conn.(*safesocket.WindowsClientConn); !ok {
|
||||
return nil, fmt.Errorf("not a WindowsClientConn: %T", ci.conn)
|
||||
}
|
||||
|
||||
// We duplicate the token's handle so that the WindowsToken we return may have
|
||||
// a lifetime independent from the original connection.
|
||||
var h windows.Handle
|
||||
if err := windows.DuplicateHandle(
|
||||
windows.CurrentProcess(),
|
||||
windows.Handle(wcc.Token()),
|
||||
windows.CurrentProcess(),
|
||||
&h,
|
||||
0,
|
||||
false,
|
||||
windows.DUPLICATE_SAME_ACCESS,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &token{t: windows.Token(h)}
|
||||
runtime.SetFinalizer(result, func(t *token) { t.Close() })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -24,12 +24,10 @@ import (
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/posture"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -69,14 +67,6 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
|
||||
}
|
||||
case "/posture/identity":
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
b.handleC2NPostureIdentityGet(w, r)
|
||||
default:
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
@@ -225,50 +215,18 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /posture/identity received")
|
||||
|
||||
res := tailcfg.C2NPostureIdentityResponse{}
|
||||
|
||||
// Only collect serial numbers if enabled on the client,
|
||||
// this will first check syspolicy, MDM settings like Registry
|
||||
// on Windows or defaults on macOS. If they are not set, it falls
|
||||
// back to the cli-flag, `--posture-checking`.
|
||||
choice, err := syspolicy.GetPreferenceOption(syspolicy.PostureChecking)
|
||||
if err != nil {
|
||||
b.logf(
|
||||
"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",
|
||||
b.Prefs().PostureChecking(),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
|
||||
sns, err := posture.GetSerialNumbers(b.logf)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res.SerialNumbers = sns
|
||||
} else {
|
||||
res.PostureDisabled = true
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
// If NewUpdater does not return an error, we can update the installation.
|
||||
// Exception: When version.IsMacSysExt returns true, we don't support that
|
||||
// yet. TODO(cpalmer, #6995): Implement it.
|
||||
//
|
||||
// Note that we create the Updater solely to check for errors; we do not
|
||||
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
|
||||
prefs := b.Prefs().AutoUpdate()
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
|
||||
return tailcfg.C2NUpdateResponse{
|
||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
|
||||
Supported: err == nil,
|
||||
Supported: err == nil && !version.IsMacSysExt(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,12 +263,9 @@ func findCmdTailscale() (string, error) {
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if self == "/usr/sbin/tailscaled" || self == "/usr/bin/tailscaled" {
|
||||
if self == "/usr/sbin/tailscaled" {
|
||||
return "/usr/bin/tailscale", nil
|
||||
}
|
||||
if self == "/usr/local/sbin/tailscaled" || self == "/usr/local/bin/tailscaled" {
|
||||
return "/usr/local/bin/tailscale", nil
|
||||
}
|
||||
return "", errors.New("tailscale not found in expected place")
|
||||
case "windows":
|
||||
dir := filepath.Dir(self)
|
||||
|
||||
@@ -84,9 +84,11 @@ var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
|
||||
// ACME process. ACME process is used for new domain certs, existing expired
|
||||
// certs or existing certs that should get renewed due to upcoming expiry.
|
||||
//
|
||||
// If a cert is expired, it will be renewed synchronously otherwise it will be
|
||||
// renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
// syncRenewal changes renewal behavior for existing certs that are still valid
|
||||
// but need renewal. When syncRenewal is set, the method blocks until a new
|
||||
// cert is issued. When syncRenewal is not set, existing cert is returned right
|
||||
// away and renewal is kicked off in a background goroutine.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
@@ -106,16 +108,18 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
}
|
||||
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// If we got here, we have a valid unexpired cert.
|
||||
// Check whether we should start an async renewal.
|
||||
if shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair); err != nil {
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if shouldRenew {
|
||||
} else if !shouldRenew {
|
||||
return pair, nil
|
||||
}
|
||||
if !syncRenewal {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now)
|
||||
}
|
||||
return pair, nil
|
||||
// Synchronous renewal happens below.
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
|
||||
@@ -126,8 +130,6 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
// shouldStartDomainRenewal reports whether the domain's cert should be renewed
|
||||
// based on the current time, the cert's expiry, and the ARI check.
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
@@ -363,9 +365,8 @@ type TLSCertKeyPair struct {
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
// getCertPEMCached returns a non-nil keyPair if a cached keypair for domain
|
||||
// exists on disk in dir that is valid at the provided now time.
|
||||
//
|
||||
// getCertPEMCached returns a non-nil keyPair and true if a cached keypair for
|
||||
// domain exists on disk in dir that is valid at the provided now time.
|
||||
// If the keypair is expired, it returns errCertExpired.
|
||||
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
|
||||
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
|
||||
@@ -12,6 +12,6 @@ type TLSCertKeyPair struct {
|
||||
CertPEM, KeyPEM []byte
|
||||
}
|
||||
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
|
||||
return nil, errors.New("not implemented for js/wasm")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -43,7 +44,6 @@ import (
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
@@ -157,7 +157,6 @@ type LocalBackend struct {
|
||||
e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys
|
||||
store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys
|
||||
dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys
|
||||
pushDeviceToken syncs.AtomicValue[string]
|
||||
backendLogID logid.PublicID
|
||||
unregisterNetMon func()
|
||||
unregisterHealthWatch func()
|
||||
@@ -199,8 +198,7 @@ type LocalBackend struct {
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
conf *conffile.Config // latest parsed config, or nil if not in declarative mode
|
||||
pm *profileManager // mu guards access
|
||||
pm *profileManager // mu guards access
|
||||
filterHash deephash.Sum
|
||||
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
|
||||
ccGen clientGen // function for producing controlclient; lazily populated
|
||||
@@ -241,6 +239,7 @@ type LocalBackend struct {
|
||||
peerAPIServer *peerAPIServer // or nil
|
||||
peerAPIListeners []*peerAPIListener
|
||||
loginFlags controlclient.LoginFlags
|
||||
incomingFiles map[*incomingFile]bool
|
||||
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
|
||||
notifyWatchers set.HandleSet[*watchSession]
|
||||
lastStatusTime time.Time // status.AsOf value of the last processed status update
|
||||
@@ -261,7 +260,6 @@ type LocalBackend struct {
|
||||
componentLogUntil map[string]componentLogState
|
||||
// c2nUpdateStatus is the status of c2n-triggered client update.
|
||||
c2nUpdateStatus updateStatus
|
||||
currentUser ipnauth.WindowsToken
|
||||
|
||||
// ServeConfig fields. (also guarded by mu)
|
||||
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
||||
@@ -269,7 +267,7 @@ type LocalBackend struct {
|
||||
activeWatchSessions set.Set[string] // of WatchIPN SessionID
|
||||
|
||||
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
|
||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
|
||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -323,18 +321,8 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
sds.SetDialer(dialer.SystemDial)
|
||||
}
|
||||
|
||||
if sys.InitialConfig != nil {
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
mp, err := sys.InitialConfig.Parsed.ToPrefs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.ApplyEdits(&mp)
|
||||
if err := pm.SetPrefs(p.View(), ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
hi := hostinfo.New()
|
||||
logf.JSON(1, "Hostinfo", hi)
|
||||
envknob.LogCurrent(logf)
|
||||
if dialer == nil {
|
||||
dialer = &tsdial.Dialer{Logf: logf}
|
||||
@@ -353,7 +341,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
sys: sys,
|
||||
conf: sys.InitialConfig,
|
||||
e: e,
|
||||
dialer: dialer,
|
||||
store: store,
|
||||
@@ -532,25 +519,6 @@ func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
|
||||
b.directFileDoFinalRename = v
|
||||
}
|
||||
|
||||
// ReloadCOnfig reloads the backend's config from disk.
|
||||
//
|
||||
// It returns (false, nil) if not running in declarative mode, (true, nil) on
|
||||
// success, or (false, error) on failure.
|
||||
func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.conf == nil {
|
||||
return false, nil
|
||||
}
|
||||
conf, err := conffile.Load(b.conf.Path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b.conf = conf
|
||||
// TODO(bradfitz): apply things
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// pauseOrResumeControlClientLocked pauses b.cc if there is no network available
|
||||
// or if the LocalBackend is in Stopped state with a valid NetMap. In all other
|
||||
// cases, it unpauses it. It is a no-op if b.cc is nil.
|
||||
@@ -647,9 +615,6 @@ func (b *LocalBackend) Shutdown() {
|
||||
if b.sockstatLogger != nil {
|
||||
b.sockstatLogger.Shutdown()
|
||||
}
|
||||
if b.peerAPIServer != nil {
|
||||
b.peerAPIServer.taildrop.Shutdown()
|
||||
}
|
||||
|
||||
b.unregisterNetMon()
|
||||
b.unregisterHealthWatch()
|
||||
@@ -1487,17 +1452,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
}
|
||||
profileID := b.pm.CurrentProfile().ID
|
||||
if b.state != ipn.Running && b.conf != nil && b.conf.Parsed.AuthKey != nil && opts.AuthKey == "" {
|
||||
v := *b.conf.Parsed.AuthKey
|
||||
if filename, ok := strings.CutPrefix(v, "file:"); ok {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading config file authKey: %w", err)
|
||||
}
|
||||
v = strings.TrimSpace(string(b))
|
||||
}
|
||||
opts.AuthKey = v
|
||||
}
|
||||
|
||||
// The iOS client sends a "Start" whenever its UI screen comes
|
||||
// up, just because it wants a netmap. That should be fixed,
|
||||
@@ -1520,12 +1474,10 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
|
||||
hostinfo := hostinfo.New()
|
||||
applyConfigToHostinfo(hostinfo, b.conf)
|
||||
hostinfo.BackendLogID = b.backendLogID.String()
|
||||
hostinfo.FrontendLogID = opts.FrontendLogID
|
||||
hostinfo.Userspace.Set(b.sys.IsNetstack())
|
||||
hostinfo.UserspaceRouter.Set(b.sys.IsNetstackRouter())
|
||||
b.logf.JSON(1, "Hostinfo", hostinfo)
|
||||
|
||||
// TODO(apenwarr): avoid the need to reinit controlclient.
|
||||
// This will trigger a full relogin/reconfigure cycle every
|
||||
@@ -1675,7 +1627,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
tkaHead = string(head)
|
||||
}
|
||||
confWantRunning := b.conf != nil && wantRunning
|
||||
b.mu.Unlock()
|
||||
|
||||
if endpoints != nil {
|
||||
@@ -1690,7 +1641,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.send(ipn.Notify{BackendLogID: &blid})
|
||||
b.send(ipn.Notify{Prefs: &prefs})
|
||||
|
||||
if !loggedOut && (b.hasNodeKey() || confWantRunning) {
|
||||
if !loggedOut && b.hasNodeKey() {
|
||||
// Even if !WantRunning, we should verify our key, if there
|
||||
// is one. If you want tailscaled to be completely idle,
|
||||
// use logout instead.
|
||||
@@ -2026,9 +1977,10 @@ func (b *LocalBackend) readPoller() {
|
||||
b.hostinfo = new(tailcfg.Hostinfo)
|
||||
}
|
||||
b.hostinfo.Services = sl
|
||||
hi := b.hostinfo
|
||||
b.mu.Unlock()
|
||||
|
||||
b.doSetHostinfoFilterServices()
|
||||
b.doSetHostinfoFilterServices(hi)
|
||||
|
||||
if isFirst {
|
||||
isFirst = false
|
||||
@@ -2037,28 +1989,19 @@ func (b *LocalBackend) readPoller() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetPushDeviceToken returns the push notification device token.
|
||||
func (b *LocalBackend) GetPushDeviceToken() string {
|
||||
return b.pushDeviceToken.Load()
|
||||
}
|
||||
// ResendHostinfoIfNeeded is called to recompute the Hostinfo and send
|
||||
// the new version to the control server.
|
||||
func (b *LocalBackend) ResendHostinfoIfNeeded() {
|
||||
hi := hostinfo.New()
|
||||
|
||||
// SetPushDeviceToken sets the push notification device token and informs the
|
||||
// controlclient of the new value.
|
||||
func (b *LocalBackend) SetPushDeviceToken(tk string) {
|
||||
old := b.pushDeviceToken.Swap(tk)
|
||||
if old == tk {
|
||||
return
|
||||
b.mu.Lock()
|
||||
if b.hostinfo != nil {
|
||||
hi.Services = b.hostinfo.Services
|
||||
}
|
||||
b.doSetHostinfoFilterServices()
|
||||
}
|
||||
b.hostinfo = hi
|
||||
b.mu.Unlock()
|
||||
|
||||
func applyConfigToHostinfo(hi *tailcfg.Hostinfo, c *conffile.Config) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if c.Parsed.Hostname != nil {
|
||||
hi.Hostname = *c.Parsed.Hostname
|
||||
}
|
||||
b.doSetHostinfoFilterServices(hi)
|
||||
}
|
||||
|
||||
// WatchNotifications subscribes to the ipn.Notify message bus notification
|
||||
@@ -2214,12 +2157,6 @@ func (b *LocalBackend) DebugForceNetmapUpdate() {
|
||||
b.setNetMapLocked(nm)
|
||||
}
|
||||
|
||||
// DebugPickNewDERP forwards to magicsock.Conn.DebugPickNewDERP.
|
||||
// See its docs.
|
||||
func (b *LocalBackend) DebugPickNewDERP() error {
|
||||
return b.sys.MagicSock.Get().DebugPickNewDERP()
|
||||
}
|
||||
|
||||
// send delivers n to the connected frontend and any API watchers from
|
||||
// LocalBackend.WatchNotifications (via the LocalAPI).
|
||||
//
|
||||
@@ -2276,7 +2213,10 @@ func (b *LocalBackend) sendFileNotify() {
|
||||
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
|
||||
// in JSON to clients. They distinguish between empty and non-nil
|
||||
// to know whether a Notify should be able about files.
|
||||
n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
|
||||
n.IncomingFiles = make([]ipn.PartialFile, 0)
|
||||
for f := range b.incomingFiles {
|
||||
n.IncomingFiles = append(n.IncomingFiles, f.PartialFile())
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
sort.Slice(n.IncomingFiles, func(i, j int) bool {
|
||||
@@ -2341,7 +2281,16 @@ func (b *LocalBackend) onClientVersion(v *tailcfg.ClientVersion) {
|
||||
b.mu.Lock()
|
||||
b.lastClientVersion = v
|
||||
b.mu.Unlock()
|
||||
b.send(ipn.Notify{ClientVersion: v})
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "ios":
|
||||
// These auto-update well enough, and we haven't converted the
|
||||
// ClientVersion types to Swift yet, so don't send them in ipn.Notify
|
||||
// messages.
|
||||
default:
|
||||
// But everything else is a Go client and can deal with this field, even
|
||||
// if they ignore it.
|
||||
b.send(ipn.Notify{ClientVersion: v})
|
||||
}
|
||||
}
|
||||
|
||||
// For testing lazy machine key generation.
|
||||
@@ -2723,7 +2672,7 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
return !p.ShieldsUp() && b.netMap.CollectServices
|
||||
}
|
||||
|
||||
// SetCurrentUser is used to implement support for multi-user systems (only
|
||||
// SetCurrentUserID is used to implement support for multi-user systems (only
|
||||
// Windows 2022-11-25). On such systems, the uid is used to determine which
|
||||
// user's state should be used. The current user is maintained by active
|
||||
// connections open to the backend.
|
||||
@@ -2738,35 +2687,18 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
// unattended mode. The user must disable unattended mode before the user can be
|
||||
// changed.
|
||||
//
|
||||
// On non-multi-user systems, the token should be set to nil.
|
||||
//
|
||||
// SetCurrentUser returns the ipn.WindowsUserID associated with token
|
||||
// when successful.
|
||||
func (b *LocalBackend) SetCurrentUser(token ipnauth.WindowsToken) (ipn.WindowsUserID, error) {
|
||||
var uid ipn.WindowsUserID
|
||||
if token != nil {
|
||||
var err error
|
||||
uid, err = token.UID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// On non-multi-user systems, the uid should be set to empty string.
|
||||
func (b *LocalBackend) SetCurrentUserID(uid ipn.WindowsUserID) {
|
||||
b.mu.Lock()
|
||||
if b.pm.CurrentUserID() == uid {
|
||||
b.mu.Unlock()
|
||||
return uid, nil
|
||||
return
|
||||
}
|
||||
if err := b.pm.SetCurrentUserID(uid); err != nil {
|
||||
b.mu.Unlock()
|
||||
return uid, nil
|
||||
return
|
||||
}
|
||||
if b.currentUser != nil {
|
||||
b.currentUser.Close()
|
||||
}
|
||||
b.currentUser = token
|
||||
b.resetForProfileChangeLockedOnEntry()
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
@@ -2775,19 +2707,7 @@ func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
return b.checkPrefsLocked(p)
|
||||
}
|
||||
|
||||
// isConfigLocked_Locked reports whether the parsed config file is locked.
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) isConfigLocked_Locked() bool {
|
||||
// TODO(bradfitz,maisem): make this more fine-grained, permit changing
|
||||
// some things if they're not explicitly set in the config. But for now
|
||||
// (2023-10-16), just blanket disable everything.
|
||||
return b.conf != nil && !b.conf.Parsed.Locked.EqualBool(false)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
if b.isConfigLocked_Locked() {
|
||||
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
||||
}
|
||||
var errs []error
|
||||
if p.Hostname == "badhostname.tailscale." {
|
||||
// Keep this one just for testing.
|
||||
@@ -2901,7 +2821,7 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
if mp.EggSet {
|
||||
mp.EggSet = false
|
||||
b.egg = true
|
||||
go b.doSetHostinfoFilterServices()
|
||||
go b.doSetHostinfoFilterServices(b.hostinfo.Clone())
|
||||
}
|
||||
p0 := b.pm.CurrentPrefs()
|
||||
p1 := b.pm.CurrentPrefs().AsStruct()
|
||||
@@ -3031,7 +2951,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
|
||||
b.mu.Unlock()
|
||||
|
||||
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
|
||||
b.doSetHostinfoFilterServices()
|
||||
b.doSetHostinfoFilterServices(newHi)
|
||||
}
|
||||
|
||||
if netMap != nil {
|
||||
@@ -3157,7 +3077,11 @@ func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
//
|
||||
// TODO(danderson): we shouldn't be mangling hostinfo here after
|
||||
// painstakingly constructing it in twelvety other places.
|
||||
func (b *LocalBackend) doSetHostinfoFilterServices() {
|
||||
func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
|
||||
if hi == nil {
|
||||
b.logf("[unexpected] doSetHostinfoFilterServices with nil hostinfo")
|
||||
return
|
||||
}
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
if cc == nil {
|
||||
@@ -3165,31 +3089,23 @@ func (b *LocalBackend) doSetHostinfoFilterServices() {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if b.hostinfo == nil {
|
||||
b.mu.Unlock()
|
||||
b.logf("[unexpected] doSetHostinfoFilterServices with nil hostinfo")
|
||||
return
|
||||
}
|
||||
peerAPIServices := b.peerAPIServicesLocked()
|
||||
if b.egg {
|
||||
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg", Port: 1})
|
||||
}
|
||||
|
||||
// TODO(maisem,bradfitz): store hostinfo as a view, not as a mutable struct.
|
||||
hi := *b.hostinfo // shallow copy
|
||||
b.mu.Unlock()
|
||||
|
||||
// Make a shallow copy of hostinfo so we can mutate
|
||||
// at the Service field.
|
||||
hi2 := *hi // shallow copy
|
||||
if !b.shouldUploadServices() {
|
||||
hi.Services = []tailcfg.Service{}
|
||||
hi2.Services = []tailcfg.Service{}
|
||||
}
|
||||
// Don't mutate hi.Service's underlying array. Append to
|
||||
// the slice with no free capacity.
|
||||
c := len(hi.Services)
|
||||
hi.Services = append(hi.Services[:c:c], peerAPIServices...)
|
||||
hi.PushDeviceToken = b.pushDeviceToken.Load()
|
||||
cc.SetHostinfo(&hi)
|
||||
c := len(hi2.Services)
|
||||
hi2.Services = append(hi2.Services[:c:c], peerAPIServices...)
|
||||
cc.SetHostinfo(&hi2)
|
||||
}
|
||||
|
||||
// NetMap returns the latest cached network map received from
|
||||
@@ -3632,14 +3548,13 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
taildrop: taildrop.ManagerOptions{
|
||||
Logf: b.logf,
|
||||
Clock: tstime.DefaultClock{Clock: b.clock},
|
||||
Dir: fileRoot,
|
||||
DirectFileMode: b.directFileRoot != "",
|
||||
AvoidFinalRename: !b.directFileDoFinalRename,
|
||||
SendFileNotify: b.sendFileNotify,
|
||||
}.New(),
|
||||
taildrop: &taildrop.Handler{
|
||||
Logf: b.logf,
|
||||
Clock: b.clock,
|
||||
RootDir: fileRoot,
|
||||
DirectFileMode: b.directFileRoot != "",
|
||||
DirectFileDoFinalRename: b.directFileDoFinalRename,
|
||||
},
|
||||
}
|
||||
if dm, ok := b.sys.DNSManager.GetOK(); ok {
|
||||
ps.resolver = dm.Resolver()
|
||||
@@ -3681,7 +3596,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
b.peerAPIListeners = append(b.peerAPIListeners, pln)
|
||||
}
|
||||
|
||||
go b.doSetHostinfoFilterServices()
|
||||
go b.doSetHostinfoFilterServices(b.hostinfo.Clone())
|
||||
}
|
||||
|
||||
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
||||
@@ -3853,7 +3768,6 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
|
||||
hi.RoutableIPs = prefs.AdvertiseRoutes().AsSlice()
|
||||
hi.RequestTags = prefs.AdvertiseTags().AsSlice()
|
||||
hi.ShieldsUp = prefs.ShieldsUp()
|
||||
hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply
|
||||
|
||||
var sshHostKeys []string
|
||||
if prefs.RunSSH() && envknob.CanSSHD() {
|
||||
@@ -4130,10 +4044,6 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
|
||||
b.setNetMapLocked(nil)
|
||||
b.pm.Reset()
|
||||
if b.currentUser != nil {
|
||||
b.currentUser.Close()
|
||||
b.currentUser = nil
|
||||
}
|
||||
b.keyExpired = false
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
@@ -4414,7 +4324,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
||||
if wire := b.wantIngressLocked(); b.hostinfo != nil && b.hostinfo.WireIngress != wire {
|
||||
b.logf("Hostinfo.WireIngress changed to %v", wire)
|
||||
b.hostinfo.WireIngress = wire
|
||||
go b.doSetHostinfoFilterServices()
|
||||
go b.doSetHostinfoFilterServices(b.hostinfo.Clone())
|
||||
}
|
||||
|
||||
b.setTCPPortsIntercepted(handlePorts)
|
||||
@@ -4460,8 +4370,8 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
|
||||
backend := key.(string)
|
||||
if !backends[backend] {
|
||||
b.logf("serve: closing idle connections to %s", backend)
|
||||
value.(*httputil.ReverseProxy).Transport.(*http.Transport).CloseIdleConnections()
|
||||
b.serveProxyHandlers.Delete(backend)
|
||||
value.(*reverseProxy).close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -4605,9 +4515,6 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
if !b.peerIsTaildropTargetLocked(p) {
|
||||
continue
|
||||
}
|
||||
if p.Hostinfo().OS() == "tvOS" {
|
||||
continue
|
||||
}
|
||||
peerAPI := peerAPIBase(b.netMap, p)
|
||||
if peerAPI == "" {
|
||||
continue
|
||||
@@ -4683,6 +4590,19 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||
return cc.SetDNS(ctx, req)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.incomingFiles == nil {
|
||||
b.incomingFiles = make(map[*incomingFile]bool)
|
||||
}
|
||||
if active {
|
||||
b.incomingFiles[inf] = true
|
||||
} else {
|
||||
delete(b.incomingFiles, inf)
|
||||
}
|
||||
}
|
||||
|
||||
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
|
||||
svcs := peer.Hostinfo().Services()
|
||||
for i := range svcs.LenIter() {
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -469,24 +468,6 @@ func TestFileTargets(t *testing.T) {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
|
||||
var peerMap map[tailcfg.NodeID]tailcfg.NodeView
|
||||
mak.NonNil(&peerMap)
|
||||
var nodeID tailcfg.NodeID
|
||||
nodeID = 1234
|
||||
peer := &tailcfg.Node{
|
||||
ID: 1234,
|
||||
Hostinfo: (&tailcfg.Hostinfo{OS: "tvOS"}).View(),
|
||||
}
|
||||
peerMap[nodeID] = peer.View()
|
||||
b.peers = peerMap
|
||||
got, err = b.FileTargets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
// (other cases handled by TestPeerAPIBase above)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,10 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/httphdr"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -55,7 +56,7 @@ type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
resolver *resolver.Resolver
|
||||
|
||||
taildrop *taildrop.Manager
|
||||
taildrop *taildrop.Handler
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -306,9 +307,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
if r.Method == "PUT" {
|
||||
metricPutCalls.Add(1)
|
||||
}
|
||||
metricPutCalls.Add(1)
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
@@ -587,6 +586,64 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
|
||||
fmt.Fprintln(w, "</pre>")
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
clock tstime.Clock
|
||||
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
size int64 // or -1 if unknown; never 0
|
||||
w io.Writer // underlying writer
|
||||
sendFileNotify func() // called when done
|
||||
partialPath string // non-empty in direct mode
|
||||
|
||||
mu sync.Mutex
|
||||
copied int64
|
||||
done bool
|
||||
lastNotify time.Time
|
||||
}
|
||||
|
||||
func (f *incomingFile) markAndNotifyDone() {
|
||||
f.mu.Lock()
|
||||
f.done = true
|
||||
f.mu.Unlock()
|
||||
f.sendFileNotify()
|
||||
}
|
||||
|
||||
func (f *incomingFile) Write(p []byte) (n int, err error) {
|
||||
n, err = f.w.Write(p)
|
||||
|
||||
var needNotify bool
|
||||
defer func() {
|
||||
if needNotify {
|
||||
f.sendFileNotify()
|
||||
}
|
||||
}()
|
||||
if n > 0 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.copied += int64(n)
|
||||
now := f.clock.Now()
|
||||
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
|
||||
f.lastNotify = now
|
||||
needNotify = true
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return ipn.PartialFile{
|
||||
Name: f.name,
|
||||
Started: f.started,
|
||||
DeclaredSize: f.size,
|
||||
Received: f.copied,
|
||||
PartialPath: f.partialPath,
|
||||
Done: f.done,
|
||||
}
|
||||
}
|
||||
|
||||
// canPutFile reports whether h can put a file ("Taildrop") to this node.
|
||||
func (h *peerAPIHandler) canPutFile() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
@@ -630,97 +687,129 @@ func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !envknob.CanTaildrop() {
|
||||
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.canPutFile() {
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
||||
http.Error(w, "Taildrop access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.ps.b.hasCapFileSharing() {
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
||||
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if mayDeref(h.ps.taildrop).RootDir == "" {
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if distro.Get() == distro.Unraid && !h.ps.taildrop.DirectFileMode {
|
||||
http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
prefix, ok := strings.CutPrefix(rawPath, "/v0/put/")
|
||||
suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured internals", http.StatusForbidden)
|
||||
http.Error(w, "misconfigured internals", 500)
|
||||
return
|
||||
}
|
||||
baseName, err := url.PathUnescape(prefix)
|
||||
if suffix == "" {
|
||||
http.Error(w, "empty filename", 400)
|
||||
return
|
||||
}
|
||||
if strings.Contains(suffix, "/") {
|
||||
http.Error(w, "directories not supported", 400)
|
||||
return
|
||||
}
|
||||
baseName, err := url.PathUnescape(suffix)
|
||||
if err != nil {
|
||||
http.Error(w, taildrop.ErrInvalidFileName.Error(), http.StatusBadRequest)
|
||||
http.Error(w, "bad path encoding", 400)
|
||||
return
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
id := taildrop.ClientID(h.peerNode.StableID())
|
||||
if prefix == "" {
|
||||
// List all the partial files.
|
||||
files, err := h.ps.taildrop.PartialFiles(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := enc.Encode(files); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
h.logf("json.Encoder.Encode error: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Stream all the block hashes for the specified file.
|
||||
next, close, err := h.ps.taildrop.HashPartialFile(id, baseName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer close()
|
||||
for {
|
||||
switch cs, err := next(); {
|
||||
case err == io.EOF:
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
h.logf("HashPartialFile.next error: %v", err)
|
||||
return
|
||||
default:
|
||||
if err := enc.Encode(cs); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
h.logf("json.Encoder.Encode error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "PUT":
|
||||
t0 := h.ps.b.clock.Now()
|
||||
id := taildrop.ClientID(h.peerNode.StableID())
|
||||
|
||||
var offset int64
|
||||
if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
|
||||
ranges, ok := httphdr.ParseRange(rangeHdr)
|
||||
if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
|
||||
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offset = ranges[0].Start
|
||||
}
|
||||
n, err := h.ps.taildrop.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
|
||||
switch err {
|
||||
case nil:
|
||||
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
|
||||
h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
|
||||
io.WriteString(w, "{}\n")
|
||||
case taildrop.ErrNoTaildrop:
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
case taildrop.ErrInvalidFileName:
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
case taildrop.ErrFileExists:
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
default:
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "expected method GET or PUT", http.StatusMethodNotAllowed)
|
||||
dstFile, ok := h.ps.taildrop.DiskPath(baseName)
|
||||
if !ok {
|
||||
http.Error(w, "bad filename", 400)
|
||||
return
|
||||
}
|
||||
t0 := h.ps.b.clock.Now()
|
||||
// TODO(bradfitz): prevent same filename being sent by two peers at once
|
||||
|
||||
// prevent same filename being sent twice
|
||||
if _, err := os.Stat(dstFile); err == nil {
|
||||
http.Error(w, "file exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
partialFile := dstFile + taildrop.PartialSuffix
|
||||
f, err := os.Create(partialFile)
|
||||
if err != nil {
|
||||
h.logf("put Create error: %v", taildrop.RedactErr(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success {
|
||||
os.Remove(partialFile)
|
||||
}
|
||||
}()
|
||||
var finalSize int64
|
||||
var inFile *incomingFile
|
||||
if r.ContentLength != 0 {
|
||||
inFile = &incomingFile{
|
||||
clock: h.ps.b.clock,
|
||||
name: baseName,
|
||||
started: h.ps.b.clock.Now(),
|
||||
size: r.ContentLength,
|
||||
w: f,
|
||||
sendFileNotify: h.ps.b.sendFileNotify,
|
||||
}
|
||||
if h.ps.taildrop.DirectFileMode {
|
||||
inFile.partialPath = partialFile
|
||||
}
|
||||
h.ps.b.registerIncomingFile(inFile, true)
|
||||
defer h.ps.b.registerIncomingFile(inFile, false)
|
||||
n, err := io.Copy(inFile, r.Body)
|
||||
if err != nil {
|
||||
err = taildrop.RedactErr(err)
|
||||
f.Close()
|
||||
h.logf("put Copy error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
finalSize = n
|
||||
}
|
||||
if err := taildrop.RedactErr(f.Close()); err != nil {
|
||||
h.logf("put Close error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.taildrop.DirectFileMode && !h.ps.taildrop.DirectFileDoFinalRename {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
} else {
|
||||
if err := os.Rename(partialFile, dstFile); err != nil {
|
||||
err = taildrop.RedactErr(err)
|
||||
h.logf("put final rename: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
|
||||
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
|
||||
|
||||
// TODO: set modtime
|
||||
// TODO: some real response
|
||||
success = true
|
||||
io.WriteString(w, "{}\n")
|
||||
h.ps.taildrop.KnownEmpty.Store(false)
|
||||
h.ps.b.sendFileNotify()
|
||||
}
|
||||
|
||||
func approxSize(n int64) string {
|
||||
@@ -793,7 +882,7 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
dh := health.DebugHandler("dnsfwd")
|
||||
if dh == nil {
|
||||
http.Error(w, "not wired up", http.StatusInternalServerError)
|
||||
http.Error(w, "not wired up", 500)
|
||||
return
|
||||
}
|
||||
dh.ServeHTTP(w, r)
|
||||
@@ -931,9 +1020,9 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
h.logf("handleDNS fwd error: %v", err)
|
||||
if err := ctx.Err(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
} else {
|
||||
http.Error(w, "DNS forwarding error", http.StatusInternalServerError)
|
||||
http.Error(w, "DNS forwarding error", 500)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -14,12 +15,11 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -68,7 +68,7 @@ func bodyNotContains(sub string) check {
|
||||
|
||||
func fileHasSize(name string, size int) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
root := e.ph.ps.taildrop.Dir()
|
||||
root := e.ph.ps.taildrop.RootDir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
|
||||
return
|
||||
@@ -84,7 +84,7 @@ func fileHasSize(name string, size int) check {
|
||||
|
||||
func fileHasContents(name string, want string) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
root := e.ph.ps.taildrop.Dir()
|
||||
root := e.ph.ps.taildrop.RootDir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check contents of %q", name)
|
||||
return
|
||||
@@ -173,7 +173,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("Taildrop disabled"),
|
||||
bodyContains("Taildrop access denied"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -183,7 +183,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("Taildrop disabled"),
|
||||
bodyContains("file sharing not enabled by Tailscale admin"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -193,7 +193,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
httpStatus(http.StatusInternalServerError),
|
||||
bodyContains("Taildrop disabled; no storage directory"),
|
||||
),
|
||||
},
|
||||
@@ -204,7 +204,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("POST", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(405),
|
||||
bodyContains("expected method GET or PUT"),
|
||||
bodyContains("expected method PUT"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -250,7 +250,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.partial", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -260,7 +260,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -270,7 +270,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/.", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -280,7 +280,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("empty filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -290,7 +290,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo/bar", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("directories not supported"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -300,7 +300,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -310,7 +310,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -320,7 +320,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -330,7 +330,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -340,7 +340,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -372,7 +372,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -382,7 +382,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%00", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -392,7 +392,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%01", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -402,7 +402,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -412,7 +412,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("invalid filename"),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -443,69 +443,23 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate_zero_length",
|
||||
name: "bad_duplicate_zero_length",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
},
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil), httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
func(t *testing.T, env *peerAPITestEnv) {
|
||||
got, err := env.ph.ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("WaitingFiles error: %v", err)
|
||||
}
|
||||
want := []apitype.WaitingFile{{Name: "foo", Size: 0}}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
},
|
||||
httpStatus(409),
|
||||
bodyContains("file exists"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate_non_zero_length_content_length",
|
||||
name: "bad_duplicate_non_zero_length_content_length",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
|
||||
},
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")), httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents"))},
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
func(t *testing.T, env *peerAPITestEnv) {
|
||||
got, err := env.ph.ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("WaitingFiles error: %v", err)
|
||||
}
|
||||
want := []apitype.WaitingFile{{Name: "foo", Size: 8}}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate_different_files",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("fizz")),
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("buzz")),
|
||||
},
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
func(t *testing.T, env *peerAPITestEnv) {
|
||||
got, err := env.ph.ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("WaitingFiles error: %v", err)
|
||||
}
|
||||
want := []apitype.WaitingFile{{Name: "foo", Size: 4}, {Name: "foo (1)", Size: 4}}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
},
|
||||
httpStatus(409),
|
||||
bodyContains("file exists"),
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -540,11 +494,9 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
if !tt.omitRoot {
|
||||
rootDir = t.TempDir()
|
||||
if e.ph.ps.taildrop == nil {
|
||||
e.ph.ps.taildrop = taildrop.ManagerOptions{
|
||||
Logf: e.logBuf.Logf,
|
||||
Dir: rootDir,
|
||||
}.New()
|
||||
e.ph.ps.taildrop = &taildrop.Handler{}
|
||||
}
|
||||
e.ph.ps.taildrop.RootDir = rootDir
|
||||
}
|
||||
for _, req := range tt.reqs {
|
||||
e.rr = httptest.NewRecorder()
|
||||
@@ -583,10 +535,11 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
capFileSharing: true,
|
||||
clock: &tstest.Clock{},
|
||||
},
|
||||
taildrop: taildrop.ManagerOptions{
|
||||
Logf: t.Logf,
|
||||
Dir: dir,
|
||||
}.New(),
|
||||
taildrop: &taildrop.Handler{
|
||||
Logf: t.Logf,
|
||||
Clock: &tstest.Clock{},
|
||||
RootDir: dir,
|
||||
},
|
||||
}
|
||||
ph := &peerAPIHandler{
|
||||
isSelf: true,
|
||||
@@ -626,6 +579,92 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests "foo.jpg.deleted" marks (for Windows).
|
||||
func TestDeletedMarkers(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ps := &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
logf: t.Logf,
|
||||
capFileSharing: true,
|
||||
},
|
||||
taildrop: &taildrop.Handler{
|
||||
RootDir: dir,
|
||||
},
|
||||
}
|
||||
|
||||
nothingWaiting := func() {
|
||||
t.Helper()
|
||||
ps.taildrop.KnownEmpty.Store(false)
|
||||
if ps.taildrop.HasFilesWaiting() {
|
||||
t.Fatal("unexpected files waiting")
|
||||
}
|
||||
}
|
||||
touch := func(base string) {
|
||||
t.Helper()
|
||||
if err := taildrop.TouchFile(filepath.Join(dir, base)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wantEmptyTempDir := func() {
|
||||
t.Helper()
|
||||
if fis, err := os.ReadDir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(fis) > 0 && runtime.GOOS != "windows" {
|
||||
for _, fi := range fis {
|
||||
t.Errorf("unexpected file in tempdir: %q", fi.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
wf, err := ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wf) != 0 {
|
||||
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err == nil {
|
||||
rc.Close()
|
||||
t.Fatal("unexpected foo.jpg open")
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
// And verify basics still work in non-deleted cases.
|
||||
touch("foo.jpg")
|
||||
touch("bar.jpg.deleted")
|
||||
if wf, err := ps.taildrop.WaitingFiles(); err != nil {
|
||||
t.Error(err)
|
||||
} else if len(wf) != 1 {
|
||||
t.Errorf("WaitingFiles = %d; want 1", len(wf))
|
||||
} else if wf[0].Name != "foo.jpg" {
|
||||
t.Errorf("unexpected waiting file %+v", wf[0])
|
||||
}
|
||||
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
|
||||
@@ -680,3 +719,67 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactErr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err func() error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "PathError",
|
||||
err: func() error {
|
||||
return &os.PathError{
|
||||
Op: "open",
|
||||
Path: "/tmp/sensitive.txt",
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
},
|
||||
want: `open redacted.41360718: file does not exist`,
|
||||
},
|
||||
{
|
||||
name: "LinkError",
|
||||
err: func() error {
|
||||
return &os.LinkError{
|
||||
Op: "symlink",
|
||||
Old: "/tmp/sensitive.txt",
|
||||
New: "/tmp/othersensitive.txt",
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
},
|
||||
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
|
||||
},
|
||||
{
|
||||
name: "something else",
|
||||
err: func() error { return errors.New("i am another error type") },
|
||||
want: `i am another error type`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// For debugging
|
||||
var i int
|
||||
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
|
||||
t.Logf("%d: %T @ %p", i, err, err)
|
||||
i++
|
||||
}
|
||||
|
||||
t.Run("Root", func(t *testing.T) {
|
||||
got := taildrop.RedactErr(tc.err()).Error()
|
||||
if got != tc.want {
|
||||
t.Errorf("err = %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
t.Run("Wrapped", func(t *testing.T) {
|
||||
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
|
||||
want := "wrapped error: " + tc.want
|
||||
|
||||
got := taildrop.RedactErr(wrapped).Error()
|
||||
if got != want {
|
||||
t.Errorf("err = %q; want %q", got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,26 +23,18 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeHeader = "Content-Type"
|
||||
grpcBaseContentType = "application/grpc"
|
||||
)
|
||||
|
||||
// ErrETagMismatch signals that the given
|
||||
// If-Match header does not match with the
|
||||
// current etag of a resource.
|
||||
@@ -168,9 +160,8 @@ func (s *serveListener) shouldWarnAboutListenError(err error) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// handleServeListenersAccept accepts connections for the Listener. It calls the
|
||||
// handler in a new goroutine for each accepted connection. This is used to
|
||||
// handle local "tailscale serve" traffic originating from the machine itself.
|
||||
// handleServeListenersAccept accepts connections for the Listener.
|
||||
// Calls incoming handler in a new goroutine for each accepted connection.
|
||||
func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
@@ -180,7 +171,7 @@ func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
|
||||
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
|
||||
handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr)
|
||||
if handler == nil {
|
||||
s.b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, s.ap.Port())
|
||||
s.b.logf("serve RST for %v", srcAddr)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
@@ -245,9 +236,6 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
if config.IsFunnelOn() && prefs.ShieldsUp() {
|
||||
return errors.New("Unable to turn on Funnel while shields-up is enabled")
|
||||
}
|
||||
if b.isConfigLocked_Locked() {
|
||||
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
||||
}
|
||||
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
@@ -337,43 +325,32 @@ func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
|
||||
return b.setServeConfigLocked(sc, "")
|
||||
}
|
||||
|
||||
// HandleIngressTCPConn handles a TCP connection initiated by the ingressPeer
|
||||
// proxied to the local node over the PeerAPI.
|
||||
// Target represents the destination HostPort of the conn.
|
||||
// srcAddr represents the source AddrPort and not that of the ingressPeer.
|
||||
// getConnOrReset is a callback to get the connection, or reset if the connection
|
||||
// is no longer available.
|
||||
// sendRST is a callback to send a TCP RST to the ingressPeer indicating that
|
||||
// the connection was not accepted.
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
||||
// TODO(maisem,bradfitz): make this not alloc for every conn.
|
||||
logf := logger.WithPrefix(b.logf, "handleIngress: ")
|
||||
|
||||
if !sc.Valid() {
|
||||
logf("got ingress conn w/o serveConfig; rejecting")
|
||||
b.logf("localbackend: got ingress conn w/o serveConfig; rejecting")
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
if !sc.HasFunnelForTarget(target) {
|
||||
logf("got ingress conn for unconfigured %q; rejecting", target)
|
||||
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
_, port, err := net.SplitHostPort(string(target))
|
||||
if err != nil {
|
||||
logf("got ingress conn for bad target %q; rejecting", target)
|
||||
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
port16, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
logf("got ingress conn for bad target %q; rejecting", target)
|
||||
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
@@ -383,7 +360,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
||||
if handler != nil {
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
@@ -394,13 +371,12 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
||||
// extend serveHTTPContext or similar.
|
||||
handler := b.tcpHandlerForServe(dport, srcAddr)
|
||||
if handler == nil {
|
||||
logf("[unexpected] no matching ingress serve handler for %v to port %v", srcAddr, dport)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
@@ -414,11 +390,13 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
||||
b.mu.Unlock()
|
||||
|
||||
if !sc.Valid() {
|
||||
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport)
|
||||
return nil
|
||||
}
|
||||
|
||||
tcph, ok := sc.FindTCP(dport)
|
||||
if !ok {
|
||||
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -462,7 +440,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, sni)
|
||||
pair, err := b.GetCertPEM(ctx, sni, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -490,6 +468,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -545,92 +524,23 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
||||
|
||||
// proxyHandlerForBackend creates a new HTTP reverse proxy for a particular backend that
|
||||
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
|
||||
func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) {
|
||||
func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.ReverseProxy, error) {
|
||||
targetURL, insecure := expandProxyArg(backend)
|
||||
u, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||
}
|
||||
p := &reverseProxy{
|
||||
logf: b.logf,
|
||||
url: u,
|
||||
insecure: insecure,
|
||||
backend: backend,
|
||||
lb: b,
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// reverseProxy is a proxy that forwards a request to a backend host
|
||||
// (preconfigured via ipn.ServeConfig). If the host is configured with
|
||||
// http+insecure prefix, connection between proxy and backend will be over
|
||||
// insecure TLS. If the backend host has a http prefix and the incoming request
|
||||
// has application/grpc content type header, the connection will be over h2c.
|
||||
// Otherwise standard Go http transport will be used.
|
||||
type reverseProxy struct {
|
||||
logf logger.Logf
|
||||
url *url.URL
|
||||
// insecure tracks whether the connection to an https backend should be
|
||||
// insecure (i.e because we cannot verify its CA).
|
||||
insecure bool
|
||||
backend string
|
||||
lb *LocalBackend
|
||||
httpTransport lazy.SyncValue[*http.Transport] // transport for non-h2c backends
|
||||
h2cTransport lazy.SyncValue[*http2.Transport] // transport for h2c backends
|
||||
// closed tracks whether proxy is closed/currently closing.
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// close ensures that any open backend connections get closed.
|
||||
func (rp *reverseProxy) close() {
|
||||
rp.closed.Store(true)
|
||||
if h2cT := rp.h2cTransport.Get(func() *http2.Transport {
|
||||
return nil
|
||||
}); h2cT != nil {
|
||||
h2cT.CloseIdleConnections()
|
||||
}
|
||||
if httpTransport := rp.httpTransport.Get(func() *http.Transport {
|
||||
return nil
|
||||
}); httpTransport != nil {
|
||||
httpTransport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if closed := rp.closed.Load(); closed {
|
||||
rp.logf("received a request for a proxy that's being closed or has been closed")
|
||||
http.Error(w, "proxy is closed", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
p := &httputil.ReverseProxy{Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(rp.url)
|
||||
r.Out.Host = r.In.Host
|
||||
addProxyForwardedHeaders(r)
|
||||
rp.lb.addTailscaleIdentityHeaders(r)
|
||||
}}
|
||||
|
||||
// There is no way to autodetect h2c as per RFC 9113
|
||||
// https://datatracker.ietf.org/doc/html/rfc9113#name-starting-http-2.
|
||||
// However, we assume that http:// proxy prefix in combination with the
|
||||
// protoccol being HTTP/2 is sufficient to detect h2c for our needs. Only use this for
|
||||
// gRPC to fix a known problem of plaintext gRPC backends
|
||||
if rp.shouldProxyViaH2C(r) {
|
||||
rp.logf("received a proxy request for plaintext gRPC")
|
||||
p.Transport = rp.getH2CTransport()
|
||||
} else {
|
||||
p.Transport = rp.getTransport()
|
||||
}
|
||||
p.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getTransport returns the Transport used for regular (non-GRPC) requests
|
||||
// to the backend. The Transport gets created lazily, at most once.
|
||||
func (rp *reverseProxy) getTransport() *http.Transport {
|
||||
return rp.httpTransport.Get(func() *http.Transport {
|
||||
return &http.Transport{
|
||||
DialContext: rp.lb.dialer.SystemDial,
|
||||
rp := &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(u)
|
||||
r.Out.Host = r.In.Host
|
||||
addProxyForwardedHeaders(r)
|
||||
b.addTailscaleIdentityHeaders(r)
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
DialContext: b.dialer.SystemDial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: rp.insecure,
|
||||
InsecureSkipVerify: insecure,
|
||||
},
|
||||
// Values for the following parameters have been copied from http.DefaultTransport.
|
||||
ForceAttemptHTTP2: true,
|
||||
@@ -638,38 +548,9 @@ func (rp *reverseProxy) getTransport() *http.Transport {
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getH2CTransport returns the Transport used for GRPC requests to the backend.
|
||||
// The Transport gets created lazily, at most once.
|
||||
func (rp *reverseProxy) getH2CTransport() *http2.Transport {
|
||||
return rp.h2cTransport.Get(func() *http2.Transport {
|
||||
return &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
DialTLSContext: func(ctx context.Context, network string, addr string, _ *tls.Config) (net.Conn, error) {
|
||||
return rp.lb.dialer.SystemDial(ctx, "tcp", rp.url.Host)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// This is not a generally reliable way how to determine whether a request is
|
||||
// for a h2c server, but sufficient for our particular use case.
|
||||
func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
||||
contentType := r.Header.Get(contentTypeHeader)
|
||||
return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
|
||||
}
|
||||
|
||||
// isGRPC accepts an HTTP request's content type header value and determines
|
||||
// whether this is gRPC content. grpc-go considers a value that equals
|
||||
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
|
||||
// valid grpc content type header.
|
||||
// https://github.com/grpc/grpc-go/blob/v1.60.0-dev/internal/grpcutil/method.go#L41-L78
|
||||
func isGRPCContentType(contentType string) bool {
|
||||
s, ok := strings.CutPrefix(contentType, grpcBaseContentType)
|
||||
return ok && (len(s) == 0 || s[0] == '+' || s[0] == ';')
|
||||
},
|
||||
}
|
||||
return rp, nil
|
||||
}
|
||||
|
||||
func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
|
||||
@@ -866,7 +747,7 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHe
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, hi.ServerName)
|
||||
pair, err := b.GetCertPEM(ctx, hi.ServerName, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -447,119 +446,6 @@ func TestServeHTTPProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_reverseProxyConfiguration(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
type test struct {
|
||||
backend string
|
||||
path string
|
||||
// set to false to test that a proxy has been removed
|
||||
shouldExist bool
|
||||
wantsInsecure bool
|
||||
wantsURL url.URL
|
||||
}
|
||||
runner := func(name string, tests []test) {
|
||||
t.Logf("running tests for %s", name)
|
||||
host := ipn.HostPort("http://example.ts.net:80")
|
||||
conf := &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
host: {Handlers: map[string]*ipn.HTTPHandler{}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.shouldExist {
|
||||
conf.Web[host].Handlers[tt.path] = &ipn.HTTPHandler{Proxy: tt.backend}
|
||||
}
|
||||
}
|
||||
if err := b.setServeConfigLocked(conf, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// test that reverseproxies have been set up as expected
|
||||
for _, tt := range tests {
|
||||
rp, ok := b.serveProxyHandlers.Load(tt.backend)
|
||||
if !tt.shouldExist && ok {
|
||||
t.Errorf("proxy for backend %s should not exist, but it does", tt.backend)
|
||||
}
|
||||
if !tt.shouldExist {
|
||||
continue
|
||||
}
|
||||
parsedRp, ok := rp.(*reverseProxy)
|
||||
if !ok {
|
||||
t.Errorf("proxy for backend %q is not a reverseproxy", tt.backend)
|
||||
}
|
||||
if parsedRp.insecure != tt.wantsInsecure {
|
||||
t.Errorf("proxy for backend %q should be insecure: %v got insecure: %v", tt.backend, tt.wantsInsecure, parsedRp.insecure)
|
||||
}
|
||||
if !reflect.DeepEqual(*parsedRp.url, tt.wantsURL) {
|
||||
t.Errorf("proxy for backend %q should have URL %#+v, got URL %+#v", tt.backend, &tt.wantsURL, parsedRp.url)
|
||||
}
|
||||
if tt.backend != parsedRp.backend {
|
||||
t.Errorf("proxy for backend %q should have backend %q got %q", tt.backend, tt.backend, parsedRp.backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// configure local backend with some proxy backends
|
||||
runner("initial proxy configs", []test{
|
||||
{
|
||||
backend: "http://example.com/docs",
|
||||
path: "/example",
|
||||
shouldExist: true,
|
||||
wantsInsecure: false,
|
||||
wantsURL: mustCreateURL(t, "http://example.com/docs"),
|
||||
},
|
||||
{
|
||||
backend: "https://example1.com",
|
||||
path: "/example1",
|
||||
shouldExist: true,
|
||||
wantsInsecure: false,
|
||||
wantsURL: mustCreateURL(t, "https://example1.com"),
|
||||
},
|
||||
{
|
||||
backend: "https+insecure://example2.com",
|
||||
path: "/example2",
|
||||
shouldExist: true,
|
||||
wantsInsecure: true,
|
||||
wantsURL: mustCreateURL(t, "https://example2.com"),
|
||||
},
|
||||
})
|
||||
|
||||
// reconfigure the local backend with different proxies
|
||||
runner("reloaded proxy configs", []test{
|
||||
{
|
||||
backend: "http://example.com/docs",
|
||||
path: "/example",
|
||||
shouldExist: true,
|
||||
wantsInsecure: false,
|
||||
wantsURL: mustCreateURL(t, "http://example.com/docs"),
|
||||
},
|
||||
{
|
||||
backend: "https://example1.com",
|
||||
shouldExist: false,
|
||||
},
|
||||
{
|
||||
backend: "https+insecure://example2.com",
|
||||
shouldExist: false,
|
||||
},
|
||||
{
|
||||
backend: "https+insecure://example3.com",
|
||||
path: "/example3",
|
||||
shouldExist: true,
|
||||
wantsInsecure: true,
|
||||
wantsURL: mustCreateURL(t, "https://example3.com"),
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func mustCreateURL(t *testing.T, u string) url.URL {
|
||||
t.Helper()
|
||||
uParsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
t.Fatalf("failed parsing url: %v", err)
|
||||
}
|
||||
return *uParsed
|
||||
}
|
||||
|
||||
func newTestBackend(t *testing.T) *LocalBackend {
|
||||
sys := &tsd.System{}
|
||||
e, err := wgengine.NewUserspaceEngine(t.Logf, wgengine.Config{SetSubsystem: sys.Set})
|
||||
@@ -701,23 +587,3 @@ func TestServeFileOrDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isGRPCContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
contentType string
|
||||
want bool
|
||||
}{
|
||||
{contentType: "application/grpc", want: true},
|
||||
{contentType: "application/grpc;", want: true},
|
||||
{contentType: "application/grpc+", want: true},
|
||||
{contentType: "application/grpcfoobar"},
|
||||
{contentType: "application/text"},
|
||||
{contentType: "foobar"},
|
||||
{contentType: ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isGRPCContentType(tt.contentType); got != tt.want {
|
||||
t.Errorf("isGRPCContentType(%q) = %v, want %v", tt.contentType, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +202,6 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.netMon, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
lah.PermitCert = s.connCanFetchCerts(ci)
|
||||
lah.CallerIsLocalAdmin = s.connIsLocalAdmin(ci)
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -243,30 +242,8 @@ func (s *Server) checkConnIdentityLocked(ci *ipnauth.ConnIdentity) error {
|
||||
for _, active = range s.activeReqs {
|
||||
break
|
||||
}
|
||||
if active != nil {
|
||||
chkTok, err := ci.WindowsToken()
|
||||
if err == nil {
|
||||
defer chkTok.Close()
|
||||
} else if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
return err
|
||||
}
|
||||
|
||||
activeTok, err := active.WindowsToken()
|
||||
if err == nil {
|
||||
defer activeTok.Close()
|
||||
} else if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
return err
|
||||
}
|
||||
|
||||
if chkTok != nil && !chkTok.EqualUIDs(activeTok) {
|
||||
var b strings.Builder
|
||||
b.WriteString("Tailscale already in use")
|
||||
if username, err := activeTok.Username(); err == nil {
|
||||
fmt.Fprintf(&b, " by %s", username)
|
||||
}
|
||||
fmt.Fprintf(&b, ", pid %d", active.Pid())
|
||||
return inUseOtherUserError{errors.New(b.String())}
|
||||
}
|
||||
if active != nil && ci.WindowsUserID() != active.WindowsUserID() {
|
||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User().Username, active.Pid())}
|
||||
}
|
||||
}
|
||||
if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil {
|
||||
@@ -364,31 +341,6 @@ func (s *Server) connCanFetchCerts(ci *ipnauth.ConnIdentity) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// connIsLocalAdmin reports whether ci has administrative access to the local
|
||||
// machine, for whatever that means with respect to the current OS.
|
||||
//
|
||||
// This returns true only on Windows machines when the client user is a
|
||||
// member of the built-in Administrators group (but not necessarily elevated).
|
||||
// This is useful because, on Windows, tailscaled itself always runs with
|
||||
// elevated rights: we want to avoid privilege escalation for certain mutative operations.
|
||||
func (s *Server) connIsLocalAdmin(ci *ipnauth.ConnIdentity) bool {
|
||||
tok, err := ci.WindowsToken()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
s.logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer tok.Close()
|
||||
|
||||
isAdmin, err := tok.IsAdministrator()
|
||||
if err != nil {
|
||||
s.logf("ipnauth.WindowsToken.IsAdministrator() error: %v", err)
|
||||
return false
|
||||
}
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
// addActiveHTTPRequest adds c to the server's list of active HTTP requests.
|
||||
//
|
||||
// If the returned error may be of type inUseOtherUserError.
|
||||
@@ -420,25 +372,14 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
|
||||
|
||||
mak.Set(&s.activeReqs, req, ci)
|
||||
|
||||
if len(s.activeReqs) == 1 {
|
||||
token, err := ci.WindowsToken()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
s.logf("error obtaining access token: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Tell the LocalBackend about the identity we're now running as.
|
||||
uid, err := lb.SetCurrentUser(token)
|
||||
if err != nil {
|
||||
token.Close()
|
||||
return nil, err
|
||||
}
|
||||
if s.lastUserID != uid {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = uid
|
||||
if uid := ci.WindowsUserID(); uid != "" && len(s.activeReqs) == 1 {
|
||||
// Tell the LocalBackend about the identity we're now running as.
|
||||
lb.SetCurrentUserID(uid)
|
||||
if s.lastUserID != uid {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = uid
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
pair, err := h.b.GetCertPEM(r.Context(), domain)
|
||||
pair, err := h.b.GetCertPEM(r.Context(), domain, true)
|
||||
if err != nil {
|
||||
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
|
||||
// GetCertPEM (and everywhere) should carry info info to get whether
|
||||
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -46,7 +45,6 @@ import (
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/httphdr"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/osdiag"
|
||||
@@ -81,7 +79,6 @@ var handler = map[string]localAPIHandler{
|
||||
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
|
||||
"debug-capture": (*Handler).serveDebugCapture,
|
||||
"debug-log": (*Handler).serveDebugLog,
|
||||
"debug-web-client": (*Handler).serveDebugWebClient,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"dev-set-state-store": (*Handler).serveDevSetStateStore,
|
||||
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
|
||||
@@ -96,7 +93,6 @@ var handler = map[string]localAPIHandler{
|
||||
"ping": (*Handler).servePing,
|
||||
"prefs": (*Handler).servePrefs,
|
||||
"pprof": (*Handler).servePprof,
|
||||
"reload-config": (*Handler).reloadConfig,
|
||||
"reset-auth": (*Handler).serveResetAuth,
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
@@ -157,17 +153,6 @@ type Handler struct {
|
||||
// cert fetching access.
|
||||
PermitCert bool
|
||||
|
||||
// CallerIsLocalAdmin is whether the this handler is being invoked as a
|
||||
// result of a LocalAPI call from a user who is a local admin of the current
|
||||
// machine.
|
||||
//
|
||||
// As of 2023-10-26 it is only populated on Windows.
|
||||
//
|
||||
// It can be used to to restrict some LocalAPI operations which should only
|
||||
// be run by an admin and not unprivileged users in a computing environment
|
||||
// managed by IT admins.
|
||||
CallerIsLocalAdmin bool
|
||||
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
|
||||
@@ -294,23 +279,23 @@ func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
b, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
httpReq, err := http.NewRequest("POST", "https://unused/machine/id-token", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
resp, err := h.b.DoNoiseRequest(httpReq)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -423,52 +408,36 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveWhoIsWithBackend(w, r, h.b)
|
||||
}
|
||||
|
||||
// localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
|
||||
// by the localapi WhoIs method.
|
||||
type localBackendWhoIsMethods interface {
|
||||
WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
PeerCaps(netip.Addr) tailcfg.PeerCapMap
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "whois access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
b := h.b
|
||||
var ipp netip.AddrPort
|
||||
if v := r.FormValue("addr"); v != "" {
|
||||
if ip, err := netip.ParseAddr(v); err == nil {
|
||||
ipp = netip.AddrPortFrom(ip, 0)
|
||||
} else {
|
||||
var err error
|
||||
ipp, err = netip.ParseAddrPort(v)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'addr' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
ipp, err = netip.ParseAddrPort(v)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'addr' parameter", 400)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'addr' parameter", http.StatusBadRequest)
|
||||
http.Error(w, "missing 'addr' parameter", 400)
|
||||
return
|
||||
}
|
||||
n, u, ok := b.WhoIs(ipp)
|
||||
if !ok {
|
||||
http.Error(w, "no match for IP:port", http.StatusNotFound)
|
||||
http.Error(w, "no match for IP:port", 404)
|
||||
return
|
||||
}
|
||||
res := &apitype.WhoIsResponse{
|
||||
Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
|
||||
UserProfile: &u, // always non-nil per WhoIsResponse contract
|
||||
}
|
||||
if n.Addresses().Len() > 0 {
|
||||
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
|
||||
CapMap: b.PeerCaps(ipp.Addr()),
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -597,15 +566,13 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
case "pick-new-derp":
|
||||
err = h.b.DebugPickNewDERP()
|
||||
case "":
|
||||
err = fmt.Errorf("missing parameter 'action'")
|
||||
default:
|
||||
err = fmt.Errorf("unknown action %q", action)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
@@ -622,7 +589,7 @@ func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
@@ -850,26 +817,6 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
|
||||
servePprofFunc(w, r)
|
||||
}
|
||||
|
||||
func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ok, err := h.b.ReloadConfig()
|
||||
var res apitype.ReloadConfigResponse
|
||||
res.Reloaded = ok
|
||||
if err != nil {
|
||||
res.Err = err.Error()
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&res)
|
||||
}
|
||||
|
||||
func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "reset-auth modify access denied", http.StatusForbidden)
|
||||
@@ -915,15 +862,6 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorJSON(w, fmt.Errorf("decoding config: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// require a local admin when setting a path handler
|
||||
// TODO: roll-up this Windows-specific check into either PermitWrite
|
||||
// or a global admin escalation check.
|
||||
if shouldDenyServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h) {
|
||||
http.Error(w, "must be a Windows local admin to serve a path", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
etag := r.Header.Get("If-Match")
|
||||
if err := h.b.SetServeConfig(configIn, etag); err != nil {
|
||||
if errors.Is(err, ipnlocal.ErrETagMismatch) {
|
||||
@@ -939,16 +877,6 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func shouldDenyServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) bool {
|
||||
if goos != "windows" {
|
||||
return false
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
return false
|
||||
}
|
||||
return !h.CallerIsLocalAdmin
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
||||
@@ -991,18 +919,18 @@ func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.R
|
||||
|
||||
ipStr := r.FormValue("ip")
|
||||
if ipStr == "" {
|
||||
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid IP", http.StatusBadRequest)
|
||||
http.Error(w, "invalid IP", 400)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1079,7 +1007,7 @@ func (h *Handler) serveLoginInteractive(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
h.b.StartLoginInteractive()
|
||||
@@ -1093,7 +1021,7 @@ func (h *Handler) serveStart(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
var o ipn.Options
|
||||
@@ -1116,7 +1044,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
err := h.b.Logout(r.Context())
|
||||
@@ -1124,7 +1052,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1141,7 +1069,7 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
if err := json.NewDecoder(r.Body).Decode(mp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
@@ -1179,7 +1107,7 @@ func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
p := new(ipn.Prefs)
|
||||
if err := json.NewDecoder(r.Body).Decode(p); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
err := h.b.CheckPrefs(p)
|
||||
@@ -1203,7 +1131,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if suffix == "" {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list files", http.StatusBadRequest)
|
||||
http.Error(w, "want GET to list files", 400)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
@@ -1220,7 +1148,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
wfs, err := h.b.AwaitWaitingFiles(ctx)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1229,12 +1157,12 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
name, err := url.PathUnescape(suffix)
|
||||
if err != nil {
|
||||
http.Error(w, "bad filename", http.StatusBadRequest)
|
||||
http.Error(w, "bad filename", 400)
|
||||
return
|
||||
}
|
||||
if r.Method == "DELETE" {
|
||||
if err := h.b.DeleteFile(name); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
@@ -1242,7 +1170,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
rc, size, err := h.b.OpenFile(name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
@@ -1256,7 +1184,7 @@ func writeErrorJSON(w http.ResponseWriter, err error) {
|
||||
err = errors.New("unexpected nil error")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.WriteHeader(500)
|
||||
type E struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -1269,7 +1197,7 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list targets", http.StatusBadRequest)
|
||||
http.Error(w, "want GET to list targets", 400)
|
||||
return
|
||||
}
|
||||
fts, err := h.b.FileTargets()
|
||||
@@ -1309,12 +1237,12 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "want PUT to put file", http.StatusBadRequest)
|
||||
http.Error(w, "want PUT to put file", 400)
|
||||
return
|
||||
}
|
||||
fts, err := h.b.FileTargets()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1325,7 +1253,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
stableIDStr, filenameEscaped, ok := strings.Cut(upath, "/")
|
||||
if !ok {
|
||||
http.Error(w, "bogus URL", http.StatusBadRequest)
|
||||
http.Error(w, "bogus URL", 400)
|
||||
return
|
||||
}
|
||||
stableID := tailcfg.StableNodeID(stableIDStr)
|
||||
@@ -1338,64 +1266,20 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if ft == nil {
|
||||
http.Error(w, "node not found", http.StatusNotFound)
|
||||
http.Error(w, "node not found", 404)
|
||||
return
|
||||
}
|
||||
dstURL, err := url.Parse(ft.PeerAPIURL)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus peer URL", http.StatusInternalServerError)
|
||||
http.Error(w, "bogus peer URL", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// Before we PUT a file we check to see if there are any existing partial file and if so,
|
||||
// we resume the upload from where we left off by sending the remaining file instead of
|
||||
// the full file.
|
||||
var offset int64
|
||||
var resumeDuration time.Duration
|
||||
remainingBody := io.Reader(r.Body)
|
||||
client := &http.Client{
|
||||
Transport: h.b.Dialer().PeerAPITransport(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.Context(), "GET", dstURL.String()+"/v0/put/"+filenameEscaped, nil)
|
||||
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus peer URL", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch resp, err := client.Do(req); {
|
||||
case err != nil:
|
||||
h.logf("could not fetch remote hashes: %v", err)
|
||||
case resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotFound:
|
||||
// noop; implies older peerapi without resume support
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
h.logf("fetch remote hashes status code: %d", resp.StatusCode)
|
||||
default:
|
||||
resumeStart := time.Now()
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
offset, remainingBody, err = taildrop.ResumeReader(r.Body, func() (out taildrop.BlockChecksum, err error) {
|
||||
err = dec.Decode(&out)
|
||||
return out, err
|
||||
})
|
||||
if err != nil {
|
||||
h.logf("reader could not be fully resumed: %v", err)
|
||||
}
|
||||
resumeDuration = time.Since(resumeStart).Round(time.Millisecond)
|
||||
}
|
||||
|
||||
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, remainingBody)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus outreq", http.StatusInternalServerError)
|
||||
http.Error(w, "bogus outreq", 500)
|
||||
return
|
||||
}
|
||||
outReq.ContentLength = r.ContentLength
|
||||
if offset > 0 {
|
||||
h.logf("resuming put at offset %d after %v", offset, resumeDuration)
|
||||
rangeHdr, _ := httphdr.FormatRange([]httphdr.Range{{Start: offset, Length: 0}})
|
||||
outReq.Header.Set("Range", rangeHdr)
|
||||
if outReq.ContentLength >= 0 {
|
||||
outReq.ContentLength -= offset
|
||||
}
|
||||
}
|
||||
|
||||
rp := httputil.NewSingleHostReverseProxy(dstURL)
|
||||
rp.Transport = h.b.Dialer().PeerAPITransport()
|
||||
@@ -1408,7 +1292,7 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
@@ -1423,7 +1307,7 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET", http.StatusBadRequest)
|
||||
http.Error(w, "want GET", 400)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1468,22 +1352,22 @@ func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
http.Error(w, "want POST", 400)
|
||||
return
|
||||
}
|
||||
ipStr := r.FormValue("ip")
|
||||
if ipStr == "" {
|
||||
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid IP", http.StatusBadRequest)
|
||||
http.Error(w, "invalid IP", 400)
|
||||
return
|
||||
}
|
||||
pingTypeStr := r.FormValue("type")
|
||||
if pingTypeStr == "" {
|
||||
http.Error(w, "missing 'type' parameter", http.StatusBadRequest)
|
||||
http.Error(w, "missing 'type' parameter", 400)
|
||||
return
|
||||
}
|
||||
size := 0
|
||||
@@ -1491,15 +1375,15 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
|
||||
if sizeStr != "" {
|
||||
size, err = strconv.Atoi(sizeStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'size' parameter", http.StatusBadRequest)
|
||||
http.Error(w, "invalid 'size' parameter", 400)
|
||||
return
|
||||
}
|
||||
if size != 0 && tailcfg.PingType(pingTypeStr) != tailcfg.PingDisco {
|
||||
http.Error(w, "'size' parameter is only supported with disco pings", http.StatusBadRequest)
|
||||
http.Error(w, "'size' parameter is only supported with disco pings", 400)
|
||||
return
|
||||
}
|
||||
if size > magicsock.MaxDiscoPingSize {
|
||||
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), http.StatusBadRequest)
|
||||
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1580,10 +1464,11 @@ func (h *Handler) serveSetPushDeviceToken(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
var params apitype.SetPushDeviceTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
h.b.SetPushDeviceToken(params.PushDeviceToken)
|
||||
hostinfo.SetPushDeviceToken(params.PushDeviceToken)
|
||||
h.b.ResendHostinfoIfNeeded()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -1600,7 +1485,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
var clientMetrics []clientMetricJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1612,7 +1497,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
metric.Add(int64(m.Value))
|
||||
} else {
|
||||
if clientmetric.HasPublished(m.Name) {
|
||||
http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest)
|
||||
http.Error(w, "Already have a metric named "+m.Name, 400)
|
||||
return
|
||||
}
|
||||
var metric *clientmetric.Metric
|
||||
@@ -1622,7 +1507,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
case "gauge":
|
||||
metric = clientmetric.NewGauge(m.Name)
|
||||
default:
|
||||
http.Error(w, "Unknown metric type "+m.Type, http.StatusBadRequest)
|
||||
http.Error(w, "Unknown metric type "+m.Type, 400)
|
||||
return
|
||||
}
|
||||
metrics[m.Name] = metric
|
||||
@@ -1646,7 +1531,7 @@ func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1698,7 +1583,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var req initRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1709,7 +1594,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1732,7 +1617,7 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var req modifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1792,14 +1677,14 @@ func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
var req verifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON for verifyRequest body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1819,7 +1704,7 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||
body := io.LimitReader(r.Body, 1024*1024)
|
||||
secret, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
http.Error(w, "reading secret", http.StatusBadRequest)
|
||||
http.Error(w, "reading secret", 400)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1843,7 +1728,7 @@ func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
||||
// Require a JSON stanza for the body as an additional CSRF protection.
|
||||
var req struct{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1878,7 +1763,7 @@ func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j, err := json.MarshalIndent(updates, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1941,7 +1826,7 @@ func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Req
|
||||
|
||||
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
@@ -2152,65 +2037,6 @@ func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// serveDebugWebClient is for use by the web client to communicate with
|
||||
// the control server for browser auth sessions.
|
||||
//
|
||||
// This is an unsupported localapi endpoint and restricted to flagged
|
||||
// domains on the control side. TODO(tailscale/#14335): Rename this handler.
|
||||
func (h *Handler) serveDebugWebClient(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type reqData struct {
|
||||
ID string
|
||||
Src tailcfg.NodeID
|
||||
}
|
||||
var data reqData
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
if nm == nil || !nm.SelfNode.Valid() {
|
||||
http.Error(w, "[unexpected] no self node", 400)
|
||||
return
|
||||
}
|
||||
dst := nm.SelfNode.ID()
|
||||
|
||||
var noiseURL string
|
||||
if data.ID != "" {
|
||||
noiseURL = fmt.Sprintf("https://unused/machine/webclient/wait/%d/to/%d/%s", data.Src, dst, data.ID)
|
||||
} else {
|
||||
noiseURL = fmt.Sprintf("https://unused/machine/webclient/init/%d/to/%d", data.Src, dst)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(r.Context(), "POST", noiseURL, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.b.DoNoiseRequest(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
http.Error(w, string(body), resp.StatusCode)
|
||||
return
|
||||
}
|
||||
w.Write(body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
@@ -2255,7 +2081,7 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var logRequest logRequestJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -77,138 +73,7 @@ func TestSetPushDeviceToken(t *testing.T) {
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("res.StatusCode=%d, want 200. body: %s", res.StatusCode, body)
|
||||
}
|
||||
if got := h.b.GetPushDeviceToken(); got != want {
|
||||
if got := hostinfo.New().PushDeviceToken; got != want {
|
||||
t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type whoIsBackend struct {
|
||||
whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
peerCaps map[netip.Addr]tailcfg.PeerCapMap
|
||||
}
|
||||
|
||||
func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
return b.whoIs(ipp)
|
||||
}
|
||||
|
||||
func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
|
||||
return b.peerCaps[ip]
|
||||
}
|
||||
|
||||
// Tests that the WhoIs handler accepts either IPs or IP:ports.
|
||||
//
|
||||
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
|
||||
func TestWhoIsJustIP(t *testing.T) {
|
||||
h := &Handler{
|
||||
PermitRead: true,
|
||||
}
|
||||
for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
|
||||
rec := httptest.NewRecorder()
|
||||
t.Run(input, func(t *testing.T) {
|
||||
b := whoIsBackend{
|
||||
whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
if !strings.Contains(input, ":") {
|
||||
want := netip.MustParseAddrPort("100.101.102.103:0")
|
||||
if ipp != want {
|
||||
t.Fatalf("backend called with %v; want %v", ipp, want)
|
||||
}
|
||||
}
|
||||
return (&tailcfg.Node{
|
||||
ID: 123,
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.101.102.103/32"),
|
||||
},
|
||||
}).View(),
|
||||
tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
|
||||
true
|
||||
},
|
||||
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
|
||||
netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
|
||||
"foo": {`"bar"`},
|
||||
},
|
||||
},
|
||||
}
|
||||
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
|
||||
|
||||
var res apitype.WhoIsResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
|
||||
t.Errorf("res.Node.ID=%v, want %v", got, want)
|
||||
}
|
||||
if got, want := res.UserProfile.DisplayName, "foo"; got != want {
|
||||
t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
|
||||
}
|
||||
if got, want := len(res.CapMap), 1; got != want {
|
||||
t.Errorf("capmap size=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
configIn *ipn.ServeConfig
|
||||
h *Handler
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "linux",
|
||||
goos: "linux",
|
||||
configIn: &ipn.ServeConfig{},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "windows-not-path-handler",
|
||||
goos: "windows",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "windows-path-handler-admin",
|
||||
goos: "windows",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "windows-path-handler-not-admin",
|
||||
goos: "windows",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := shouldDenyServeConfigForGOOSAndUserContext(tt.goos, tt.configIn, tt.h)
|
||||
if got != tt.want {
|
||||
t.Errorf("shouldDenyServeConfigForGOOSAndUserContext() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,10 +200,6 @@ type Prefs struct {
|
||||
// AutoUpdatePrefs docs for more details.
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
|
||||
// PostureChecking enables the collection of information used for device
|
||||
// posture checks.
|
||||
PostureChecking bool
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
@@ -250,7 +246,6 @@ type MaskedPrefs struct {
|
||||
OperatorUserSet bool `json:",omitempty"`
|
||||
ProfileNameSet bool `json:",omitempty"`
|
||||
AutoUpdateSet bool `json:",omitempty"`
|
||||
PostureCheckingSet bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
|
||||
@@ -444,8 +439,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
|
||||
p.Persist.Equals(p2.Persist) &&
|
||||
p.ProfileName == p2.ProfileName &&
|
||||
p.AutoUpdate == p2.AutoUpdate &&
|
||||
p.PostureChecking == p2.PostureChecking
|
||||
p.AutoUpdate == p2.AutoUpdate
|
||||
}
|
||||
|
||||
func (au AutoUpdatePrefs) Pretty() string {
|
||||
|
||||
@@ -57,7 +57,6 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"OperatorUser",
|
||||
"ProfileName",
|
||||
"AutoUpdate",
|
||||
"PostureChecking",
|
||||
"Persist",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
@@ -305,16 +304,6 @@ func TestPrefsEqual(t *testing.T) {
|
||||
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{PostureChecking: true},
|
||||
&Prefs{PostureChecking: true},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{PostureChecking: true},
|
||||
&Prefs{PostureChecking: false},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equals(tt.b)
|
||||
|
||||
24
ipn/serve.go
24
ipn/serve.go
@@ -163,30 +163,6 @@ func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
|
||||
return sc.TCP[port]
|
||||
}
|
||||
|
||||
// HasPathHandler reports whether if ServeConfig has at least
|
||||
// one path handler, including foreground configs.
|
||||
func (sc *ServeConfig) HasPathHandler() bool {
|
||||
if sc.Web != nil {
|
||||
for _, webServerConfig := range sc.Web {
|
||||
for _, httpHandler := range webServerConfig.Handlers {
|
||||
if httpHandler.Path != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sc.Foreground != nil {
|
||||
for _, fgConfig := range sc.Foreground {
|
||||
if fgConfig.HasPathHandler() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
|
||||
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
|
||||
func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
||||
|
||||
@@ -43,86 +43,3 @@ func TestCheckFunnelAccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasPathHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg ServeConfig
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty-config",
|
||||
cfg: ServeConfig{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "with-bg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "with-fg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{
|
||||
443: {HTTPS: true},
|
||||
},
|
||||
Foreground: map[string]*ServeConfig{
|
||||
"abc123": {
|
||||
TCP: map[uint16]*TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "with-no-bg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[HostPort]bool{"foo.test.ts.net:443": true},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "with-no-fg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
Foreground: map[string]*ServeConfig{
|
||||
"abc123": {
|
||||
TCP: map[uint16]*TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[HostPort]bool{"foo.test.ts.net:443": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.cfg.HasPathHandler()
|
||||
if tt.want != got {
|
||||
t.Errorf("HasPathHandler() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,59 +13,57 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [gioui.org](https://pkg.go.dev/gioui.org) ([MIT](https://git.sr.ht/~eliasnaur/gio/tree/32c6a9b10d0b/LICENSE))
|
||||
- [gioui.org/cpu](https://pkg.go.dev/gioui.org/cpu) ([MIT](https://git.sr.ht/~eliasnaur/gio-cpu/tree/8d6a761490d2/LICENSE))
|
||||
- [gioui.org/shader](https://pkg.go.dev/gioui.org/shader) ([MIT](https://git.sr.ht/~eliasnaur/gio-shader/tree/v1.0.6/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.42/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.40/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.11/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.41/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.35/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.43/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.35/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.38.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.14.1/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.17.1/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.22.0/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.14.2/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.14.2/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.22/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.21/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.3/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.33/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.27/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.34/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.27/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.36.3/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.9/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.9/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.10/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout](https://pkg.go.dev/github.com/benoitkugler/textlayout) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/fonts](https://pkg.go.dev/github.com/benoitkugler/textlayout/fonts) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/fonts/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/graphite](https://pkg.go.dev/github.com/benoitkugler/textlayout/graphite) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/graphite/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/harfbuzz](https://pkg.go.dev/github.com/benoitkugler/textlayout/harfbuzz) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/harfbuzz/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.4.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/go-text/typesetting](https://pkg.go.dev/github.com/go-text/typesetting) ([BSD-3-Clause](https://github.com/go-text/typesetting/blob/0399769901d5/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.1/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/65c27093e38a/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
|
||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.5/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.0/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.0/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.0/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.56/LICENSE))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.18/LICENSE))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/93bd5cbf7fd8/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/3e8cd9d6bf63/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
|
||||
@@ -73,19 +71,19 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/intern](https://pkg.go.dev/go4.org/intern) ([BSD-3-Clause](https://github.com/go4org/intern/blob/ae77deb06f29/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
|
||||
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
|
||||
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.11.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.11.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
|
||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
|
||||
|
||||
@@ -28,7 +28,6 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.14.2/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
@@ -54,7 +53,6 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.18/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
@@ -64,12 +62,12 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))
|
||||
|
||||
@@ -14,8 +14,9 @@ Some packages may only be included on certain architectures or operating systems
|
||||
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
|
||||
- [github.com/Microsoft/go-winio](https://pkg.go.dev/github.com/Microsoft/go-winio) ([MIT](https://github.com/Microsoft/go-winio/blob/v0.6.1/LICENSE))
|
||||
- [github.com/akutz/memconn](https://pkg.go.dev/github.com/akutz/memconn) ([Apache-2.0](https://github.com/akutz/memconn/blob/v0.1.0/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/1a75b4708caa/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/anmitsu/go-shlex](https://pkg.go.dev/github.com/anmitsu/go-shlex) ([MIT](https://github.com/anmitsu/go-shlex/blob/38f4b401e2be/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.42/config/LICENSE.txt))
|
||||
@@ -36,7 +37,6 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.18/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/e994401fc077/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
@@ -71,10 +71,8 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
|
||||
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.6/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/77811a65f4ff/LICENSE.md))
|
||||
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/7bcd7bca7bc5/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
|
||||
@@ -87,13 +85,13 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.15.0:LICENSE))
|
||||
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
|
||||
@@ -11,7 +11,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
|
||||
- [github.com/Microsoft/go-winio](https://pkg.go.dev/github.com/Microsoft/go-winio) ([MIT](https://github.com/Microsoft/go-winio/blob/v0.6.1/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/1a75b4708caa/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
|
||||
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
@@ -36,7 +36,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/dff4ed649e49/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/df3128d017f4/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/84569fd814a9/LICENSE))
|
||||
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
|
||||
@@ -44,14 +44,14 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
|
||||
@@ -265,13 +265,6 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
dbg("nm-safe", "yes")
|
||||
return "network-manager", nil
|
||||
}
|
||||
if err := env.nmIsUsingResolved(); err != nil {
|
||||
// If systemd-resolved is not running at all, then we don't have any
|
||||
// other choice: we take direct control of DNS.
|
||||
dbg("nm-resolved", "no")
|
||||
return "direct", nil
|
||||
}
|
||||
|
||||
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
|
||||
@@ -270,18 +270,6 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/9687
|
||||
name: "networkmanager_endeavouros",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"search example.com localdomain",
|
||||
"nameserver 10.0.0.1"),
|
||||
nmRunning("1.44.2", false)),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
|
||||
"dns: [rc=nm resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
@@ -14,17 +13,13 @@ func resolvconfStyle() string {
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
return ""
|
||||
}
|
||||
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
|
||||
// Debian resolvconf doesn't understand --version, and
|
||||
// exits with a specific error code.
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
|
||||
return "debian"
|
||||
}
|
||||
}
|
||||
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
|
||||
return "debian"
|
||||
}
|
||||
// Treat everything else as openresolv, by far the more popular implementation.
|
||||
return "openresolv"
|
||||
}
|
||||
|
||||
@@ -39,22 +39,14 @@ import (
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
var (
|
||||
optRecursiveResolver = envknob.RegisterOptBool("TS_DNSFALLBACK_RECURSIVE_RESOLVER")
|
||||
disableRecursiveResolver = envknob.RegisterBool("TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER") // legacy pre-1.52 env knob name
|
||||
)
|
||||
var disableRecursiveResolver = envknob.RegisterBool("TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER")
|
||||
|
||||
// MakeLookupFunc creates a function that can be used to resolve hostnames
|
||||
// (e.g. as a LookupIPFallback from dnscache.Resolver).
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
return func(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
// If they've explicitly disabled the recursive resolver with the legacy
|
||||
// TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER envknob or not set the
|
||||
// newer TS_DNSFALLBACK_RECURSIVE_RESOLVER to true, then don't use the
|
||||
// recursive resolver. (tailscale/corp#15261) In the future, we might
|
||||
// change the default (the opt.Bool being unset) to mean enabled.
|
||||
if disableRecursiveResolver() || !optRecursiveResolver().EqualBool(true) {
|
||||
if disableRecursiveResolver() {
|
||||
return lookup(ctx, host, logf, netMon)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package checksum provides functions for updating checksums in parsed packets.
|
||||
package checksum
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
|
||||
// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func UpdateSrcAddr(q *packet.Parsed, src netip.Addr) {
|
||||
if src.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if src.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidSNAT = true
|
||||
q.CaptureMeta.OriginalSrc = q.Src
|
||||
|
||||
old := q.Src.Addr()
|
||||
q.Src = netip.AddrPortFrom(src, q.Src.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if src.Is6() {
|
||||
v6 := src.As16()
|
||||
copy(b[8:24], v6[:])
|
||||
updateV6PacketChecksums(q, old, src)
|
||||
} else {
|
||||
v4 := src.As4()
|
||||
copy(b[12:16], v4[:])
|
||||
updateV4PacketChecksums(q, old, src)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
|
||||
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func UpdateDstAddr(q *packet.Parsed, dst netip.Addr) {
|
||||
if dst.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if dst.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidDNAT = true
|
||||
q.CaptureMeta.OriginalDst = q.Dst
|
||||
|
||||
old := q.Dst.Addr()
|
||||
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if dst.Is6() {
|
||||
v6 := dst.As16()
|
||||
copy(b[24:36], v6[:])
|
||||
updateV6PacketChecksums(q, old, dst)
|
||||
} else {
|
||||
v4 := dst.As4()
|
||||
copy(b[16:20], v4[:])
|
||||
updateV4PacketChecksums(q, old, dst)
|
||||
}
|
||||
}
|
||||
|
||||
// updateV4PacketChecksums updates the checksums in the packet buffer.
|
||||
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, only the IP header checksum is updated.
|
||||
func updateV4PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 12 {
|
||||
// Not enough space for an IPv4 header.
|
||||
return
|
||||
}
|
||||
o4, n4 := old.As4(), new.As4()
|
||||
|
||||
// First update the checksum in the IP header.
|
||||
updateV4Checksum(p.Buffer()[10:12], o4[:], n4[:])
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
// Not enough space for a UDP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[6:8], o4[:], n4[:])
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
// Not enough space for a TCP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[16:18], o4[:], n4[:])
|
||||
case ipproto.GRE:
|
||||
if len(tr) < 6 {
|
||||
// Not enough space for a GRE header.
|
||||
return
|
||||
}
|
||||
if tr[0] == 1 { // checksum present
|
||||
updateV4Checksum(tr[4:6], o4[:], n4[:])
|
||||
}
|
||||
case ipproto.SCTP, ipproto.ICMPv4:
|
||||
// No transport layer update required.
|
||||
}
|
||||
}
|
||||
|
||||
// updateV6PacketChecksums updates the checksums in the packet buffer.
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, no checksums are updated.
|
||||
func updateV6PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 40 {
|
||||
// Not enough space for an IPv6 header.
|
||||
return
|
||||
}
|
||||
o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.ICMPv6:
|
||||
if len(tr) < header.ICMPv6MinimumSize {
|
||||
return
|
||||
}
|
||||
header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.SCTP:
|
||||
// No transport layer update required.
|
||||
}
|
||||
}
|
||||
|
||||
// updateV4Checksum calculates and updates the checksum in the packet buffer for
|
||||
// a change between old and new. The oldSum must point to the 16-bit checksum
|
||||
// field in the packet buffer that holds the old checksum value, it will be
|
||||
// updated in place.
|
||||
//
|
||||
// The old and new must be the same length, and must be an even number of bytes.
|
||||
func updateV4Checksum(oldSum, old, new []byte) {
|
||||
if len(old) != len(new) {
|
||||
panic("old and new must be the same length")
|
||||
}
|
||||
if len(old)%2 != 0 {
|
||||
panic("old and new must be of even length")
|
||||
}
|
||||
/*
|
||||
RFC 1624
|
||||
Given the following notation:
|
||||
|
||||
HC - old checksum in header
|
||||
C - one's complement sum of old header
|
||||
HC' - new checksum in header
|
||||
C' - one's complement sum of new header
|
||||
m - old value of a 16-bit field
|
||||
m' - new value of a 16-bit field
|
||||
|
||||
HC' = ~(C + (-m) + m') -- [Eqn. 3]
|
||||
HC' = ~(~HC + ~m + m')
|
||||
|
||||
This can be simplified to:
|
||||
HC' = ~(C + ~m + m') -- [Eqn. 3]
|
||||
HC' = ~C'
|
||||
C' = C + ~m + m'
|
||||
*/
|
||||
|
||||
c := uint32(^binary.BigEndian.Uint16(oldSum))
|
||||
|
||||
cPrime := c
|
||||
for len(new) > 0 {
|
||||
mNot := uint32(^binary.BigEndian.Uint16(old[:2]))
|
||||
mPrime := uint32(binary.BigEndian.Uint16(new[:2]))
|
||||
cPrime += mPrime + mNot
|
||||
new, old = new[2:], old[2:]
|
||||
}
|
||||
|
||||
// Account for overflows by adding the carry bits back into the sum.
|
||||
for (cPrime >> 16) > 0 {
|
||||
cPrime = cPrime&0xFFFF + cPrime>>16
|
||||
}
|
||||
hcPrime := ^uint16(cPrime)
|
||||
binary.BigEndian.PutUint16(oldSum, hcPrime)
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package checksum
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
func fullHeaderChecksumV4(b []byte) uint16 {
|
||||
s := uint32(0)
|
||||
for i := 0; i < len(b); i += 2 {
|
||||
if i == 10 {
|
||||
// Skip checksum field.
|
||||
continue
|
||||
}
|
||||
s += uint32(binary.BigEndian.Uint16(b[i : i+2]))
|
||||
}
|
||||
for s>>16 > 0 {
|
||||
s = s&0xFFFF + s>>16
|
||||
}
|
||||
return ^uint16(s)
|
||||
}
|
||||
|
||||
func TestHeaderChecksumsV4(t *testing.T) {
|
||||
// This is not a good enough test, because it doesn't
|
||||
// check the various packet types or the many edge cases
|
||||
// of the checksum algorithm. But it's a start.
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
packet []byte
|
||||
}{
|
||||
{
|
||||
name: "ICMPv4",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x54, 0xb7, 0x96, 0x40, 0x00, 0x40, 0x01, 0x7a, 0x06, 0x64, 0x7f, 0x3f, 0x4c, 0x64, 0x40, 0x01, 0x01, 0x08, 0x00, 0x47, 0x1a, 0x00, 0x11, 0x01, 0xac, 0xcc, 0xf5, 0x95, 0x63, 0x00, 0x00, 0x00, 0x00, 0x8d, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TLS",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x3c, 0x54, 0x29, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xac, 0x64, 0x42, 0xd4, 0x33, 0x64, 0x61, 0x98, 0x0f, 0xb1, 0x94, 0x01, 0xbb, 0x0a, 0x51, 0xce, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfb, 0xe0, 0x38, 0xf6, 0x00, 0x00, 0x02, 0x04, 0x04, 0xd8, 0x04, 0x02, 0x08, 0x0a, 0x86, 0x2b, 0xcc, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DNS",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x74, 0xe2, 0x85, 0x00, 0x00, 0x40, 0x11, 0x96, 0xb5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x42, 0xd4, 0x33, 0x00, 0x35, 0xec, 0x55, 0x00, 0x60, 0xd9, 0x19, 0xed, 0xfd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x0c, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x01, 0x6c, 0xc0, 0x15, 0xc0, 0x31, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x04, 0x8e, 0xfa, 0xbd, 0xce, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DCCP",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x28, 0x15, 0x06, 0x40, 0x00, 0x40, 0x21, 0x5f, 0x2f, 0xc0, 0xa8, 0x01, 0x1f, 0xc9, 0x0b, 0x3b, 0xad, 0x80, 0x04, 0x13, 0x89, 0x05, 0x00, 0x08, 0xdb, 0x01, 0x00, 0x00, 0x04, 0x29, 0x01, 0x6d, 0xdc, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SCTP",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x30, 0x09, 0xd9, 0x40, 0x00, 0xff, 0x84, 0x50, 0xe2, 0x0a, 0x1c, 0x06, 0x2c, 0x0a, 0x1c, 0x06, 0x2b, 0x0b, 0x80, 0x40, 0x00, 0x21, 0x44, 0x15, 0x23, 0x2b, 0xf2, 0x02, 0x4e, 0x03, 0x00, 0x00, 0x10, 0x28, 0x02, 0x43, 0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
// TODO(maisem): add test for GRE.
|
||||
}
|
||||
var p packet.Parsed
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p.Decode(tt.packet)
|
||||
t.Log(p.String())
|
||||
UpdateSrcAddr(&p, netip.MustParseAddr("100.64.0.1"))
|
||||
|
||||
got := binary.BigEndian.Uint16(tt.packet[10:12])
|
||||
want := fullHeaderChecksumV4(tt.packet[:20])
|
||||
if got != want {
|
||||
t.Fatalf("got %x want %x", got, want)
|
||||
}
|
||||
|
||||
UpdateDstAddr(&p, netip.MustParseAddr("100.64.0.2"))
|
||||
got = binary.BigEndian.Uint16(tt.packet[10:12])
|
||||
want = fullHeaderChecksumV4(tt.packet[:20])
|
||||
if got != want {
|
||||
t.Fatalf("got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatChecksumsV6UDP(t *testing.T) {
|
||||
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
|
||||
|
||||
// Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
|
||||
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
|
||||
b.Encode(&header.IPv6Fields{
|
||||
PayloadLength: header.UDPMinimumSize + 32,
|
||||
TransportProtocol: header.UDPProtocolNumber,
|
||||
HopLimit: 16,
|
||||
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
})
|
||||
udp := header.UDP(b[header.IPv6MinimumSize:])
|
||||
udp.Encode(&header.UDPFields{
|
||||
SrcPort: 42,
|
||||
DstPort: 43,
|
||||
Length: header.UDPMinimumSize + 32,
|
||||
})
|
||||
xsum := header.PseudoHeaderChecksum(
|
||||
header.UDPProtocolNumber,
|
||||
tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
uint16(header.UDPMinimumSize+32),
|
||||
)
|
||||
xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum)
|
||||
udp.SetChecksum(^udp.CalculateChecksum(xsum))
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("test broken; initial packet has incorrect checksum")
|
||||
}
|
||||
|
||||
// Parse the packet.
|
||||
var p packet.Parsed
|
||||
p.Decode(b)
|
||||
t.Log(p.String())
|
||||
|
||||
// Update the source address of the packet to be the same as the dest.
|
||||
UpdateSrcAddr(&p, a2)
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("incorrect checksum after updating source address")
|
||||
}
|
||||
|
||||
// Update the dest address of the packet to be the original source address.
|
||||
UpdateDstAddr(&p, a1)
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("incorrect checksum after updating destination address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatChecksumsV6TCP(t *testing.T) {
|
||||
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
|
||||
|
||||
// Make a fake TCP packet with no payload.
|
||||
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
|
||||
b.Encode(&header.IPv6Fields{
|
||||
PayloadLength: header.TCPMinimumSize,
|
||||
TransportProtocol: header.TCPProtocolNumber,
|
||||
HopLimit: 16,
|
||||
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
})
|
||||
tcp := header.TCP(b[header.IPv6MinimumSize:])
|
||||
tcp.Encode(&header.TCPFields{
|
||||
SrcPort: 42,
|
||||
DstPort: 43,
|
||||
SeqNum: 1,
|
||||
AckNum: 2,
|
||||
DataOffset: header.TCPMinimumSize,
|
||||
Flags: 3,
|
||||
WindowSize: 4,
|
||||
Checksum: 0,
|
||||
UrgentPointer: 5,
|
||||
})
|
||||
xsum := header.PseudoHeaderChecksum(
|
||||
header.TCPProtocolNumber,
|
||||
tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
uint16(header.TCPMinimumSize),
|
||||
)
|
||||
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
|
||||
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
|
||||
t.Fatal("test broken; initial packet has incorrect checksum")
|
||||
}
|
||||
|
||||
// Parse the packet.
|
||||
var p packet.Parsed
|
||||
p.Decode(b)
|
||||
t.Log(p.String())
|
||||
|
||||
// Update the source address of the packet to be the same as the dest.
|
||||
UpdateSrcAddr(&p, a2)
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
|
||||
t.Fatal("incorrect checksum after updating source address")
|
||||
}
|
||||
|
||||
// Update the dest address of the packet to be the original source address.
|
||||
UpdateDstAddr(&p, a1)
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
|
||||
t.Fatal("incorrect checksum after updating destination address")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user