Compare commits
19 Commits
dsnet/sync
...
v1.32.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8497daa68 | ||
|
|
8023971bff | ||
|
|
0cc397e96d | ||
|
|
46235b790d | ||
|
|
b6ce364bf7 | ||
|
|
78dec82736 | ||
|
|
7c2fdcd028 | ||
|
|
613d624bea | ||
|
|
d982963e0b | ||
|
|
cdf7ae8066 | ||
|
|
30afe38cb9 | ||
|
|
2a6afafc76 | ||
|
|
23a664325e | ||
|
|
b9e1c18578 | ||
|
|
a5340a07cf | ||
|
|
ccca9faaf8 | ||
|
|
f7c15dd0b0 | ||
|
|
a780929391 | ||
|
|
fc688fe024 |
@@ -1 +1 @@
|
||||
1.31.0
|
||||
1.32.1
|
||||
|
||||
@@ -276,6 +276,12 @@ type BugReportOpts struct {
|
||||
// Diagnose specifies whether to print additional diagnostic information to
|
||||
// the logs when generating this bugreport.
|
||||
Diagnose bool
|
||||
|
||||
// Record specifies, if non-nil, whether to perform a bugreport
|
||||
// "recording"–generating an initial log marker, then waiting for
|
||||
// this channel to be closed before finishing the request, which
|
||||
// generates another log marker.
|
||||
Record <-chan struct{}
|
||||
}
|
||||
|
||||
// BugReportWithOpts logs and returns a log marker that can be shared by the
|
||||
@@ -284,16 +290,40 @@ type BugReportOpts struct {
|
||||
// The opts type specifies options to pass to the Tailscale daemon when
|
||||
// generating this bug report.
|
||||
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
|
||||
var qparams url.Values
|
||||
qparams := make(url.Values)
|
||||
if opts.Note != "" {
|
||||
qparams.Set("note", opts.Note)
|
||||
}
|
||||
if opts.Diagnose {
|
||||
qparams.Set("diagnose", "true")
|
||||
}
|
||||
if opts.Record != nil {
|
||||
qparams.Set("record", "true")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var requestBody io.Reader
|
||||
if opts.Record != nil {
|
||||
pr, pw := io.Pipe()
|
||||
requestBody = pr
|
||||
|
||||
// This goroutine waits for the 'Record' channel to be closed,
|
||||
// and then closes the write end of our pipe to unblock the
|
||||
// reader.
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
select {
|
||||
case <-opts.Record:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// lc.send might block if opts.Record != nil; see above.
|
||||
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
|
||||
body, err := lc.send(ctx, "POST", uri, 200, nil)
|
||||
body, err := lc.send(ctx, "POST", uri, 200, requestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 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/syncs from tailscale.com/cmd/derper+
|
||||
|
||||
@@ -325,11 +325,31 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
noContentChallengeHeader = "X-Tailscale-Challenge"
|
||||
noContentResponseHeader = "X-Tailscale-Response"
|
||||
)
|
||||
|
||||
// For captive portal detection
|
||||
func serveNoContent(w http.ResponseWriter, r *http.Request) {
|
||||
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
|
||||
badChar := strings.IndexFunc(challenge, func(r rune) bool {
|
||||
return !isChallengeChar(r)
|
||||
}) != -1
|
||||
if len(challenge) <= 64 && !badChar {
|
||||
w.Header().Set(noContentResponseHeader, "response "+challenge)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func isChallengeChar(c rune) bool {
|
||||
// Semi-randomly chosen as a limited set of valid characters
|
||||
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
|
||||
('0' <= c && c <= '9') ||
|
||||
c == '.' || c == '-' || c == '_'
|
||||
}
|
||||
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -7,6 +7,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
@@ -67,3 +70,57 @@ func BenchmarkServerSTUN(b *testing.B) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNoContent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no challenge",
|
||||
},
|
||||
{
|
||||
name: "valid challenge",
|
||||
input: "input",
|
||||
want: "response input",
|
||||
},
|
||||
{
|
||||
name: "invalid challenge",
|
||||
input: "foo\x00bar",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace invalid challenge",
|
||||
input: "foo bar",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "long challenge",
|
||||
input: strings.Repeat("x", 65),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
|
||||
if tt.input != "" {
|
||||
req.Header.Set(noContentChallengeHeader, tt.input)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
serveNoContent(w, req)
|
||||
resp := w.Result()
|
||||
|
||||
if tt.want == "" {
|
||||
if h, found := resp.Header[noContentResponseHeader]; found {
|
||||
t.Errorf("got %+v; expected no response header", h)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if got := resp.Header.Get(noContentResponseHeader); got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
@@ -50,7 +51,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
counterWebSocketAccepts.Add(1)
|
||||
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
|
||||
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
|
||||
})
|
||||
|
||||
@@ -41,29 +41,44 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
default:
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{
|
||||
opts := tailscale.BugReportOpts{
|
||||
Note: note,
|
||||
Diagnose: bugReportArgs.diagnose,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bugReportArgs.record {
|
||||
outln("The initial bugreport is below; please reproduce your issue and then press Enter...")
|
||||
}
|
||||
|
||||
outln(logMarker)
|
||||
|
||||
if bugReportArgs.record {
|
||||
fmt.Scanln()
|
||||
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{})
|
||||
if !bugReportArgs.record {
|
||||
// Simple, non-record case
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outln(logMarker)
|
||||
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recording; run the request in the background
|
||||
done := make(chan struct{})
|
||||
opts.Record = done
|
||||
|
||||
type bugReportResp struct {
|
||||
marker string
|
||||
err error
|
||||
}
|
||||
resCh := make(chan bugReportResp, 1)
|
||||
go func() {
|
||||
m, err := localClient.BugReportWithOpts(ctx, opts)
|
||||
resCh <- bugReportResp{m, err}
|
||||
}()
|
||||
|
||||
outln("Recording started; please reproduce your issue and then press Enter...")
|
||||
fmt.Scanln()
|
||||
close(done)
|
||||
res := <-resCh
|
||||
|
||||
if res.err != nil {
|
||||
return res.err
|
||||
}
|
||||
|
||||
outln(res.marker)
|
||||
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ var debugCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("debug")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-seconds seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
|
||||
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
|
||||
return fs
|
||||
|
||||
@@ -501,7 +501,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
if justEditMP != nil {
|
||||
justEditMP.EggSet = true
|
||||
justEditMP.EggSet = egg
|
||||
_, err := localClient.EditPrefs(ctx, justEditMP)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 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/syncs from tailscale.com/net/netcheck+
|
||||
|
||||
@@ -241,6 +241,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/net/dns+
|
||||
tailscale.com/net/tunstats from tailscale.com/net/tstun
|
||||
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/safesocket from tailscale.com/client/tailscale+
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
|
||||
@@ -51,7 +52,7 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
netConn := wsconn.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
|
||||
@@ -459,13 +459,26 @@ func TestDialPlan(t *testing.T) {
|
||||
|
||||
const (
|
||||
testProtocolVersion = 1
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort = "40080"
|
||||
httpsPort = "40443"
|
||||
)
|
||||
|
||||
getRandomPort := func() string {
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
_, port, err := net.SplitHostPort(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort := getRandomPort()
|
||||
httpsPort := getRandomPort()
|
||||
|
||||
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -111,7 +112,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
|
||||
}
|
||||
|
||||
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
|
||||
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary)
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
|
||||
@@ -96,7 +96,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion fun
|
||||
return c
|
||||
}
|
||||
|
||||
// NewNetcheckClient returns a Client that's only able to have its DialRegion method called.
|
||||
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
|
||||
// It's used by the netcheck package.
|
||||
func NewNetcheckClient(logf logger.Logf) *Client {
|
||||
return &Client{logf: logf}
|
||||
@@ -985,7 +985,9 @@ func (c *Client) isClosed() bool {
|
||||
// Close closes the client. It will not automatically reconnect after
|
||||
// being closed.
|
||||
func (c *Client) Close() error {
|
||||
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
|
||||
if c.cancelCtx != nil {
|
||||
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -28,6 +29,6 @@ func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("websocket: connected to %v", urlStr)
|
||||
netConn := websocket.NetConn(context.Background(), c, websocket.MessageBinary)
|
||||
netConn := wsconn.NetConn(context.Background(), c, websocket.MessageBinary)
|
||||
return netConn, nil
|
||||
}
|
||||
|
||||
@@ -198,6 +198,14 @@ type LocalBackend struct {
|
||||
// dialPlan is any dial plan that we've received from the control
|
||||
// server during a previous connection; it is cleared on logout.
|
||||
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
|
||||
|
||||
// tkaSyncLock is used to make tkaSyncIfNeeded an exclusive
|
||||
// section. This is needed to stop two map-responses in quick succession
|
||||
// from racing each other through TKA sync logic / RPCs.
|
||||
//
|
||||
// tkaSyncLock MUST be taken before mu (or inversely, mu must not be held
|
||||
// at the moment that tkaSyncLock is taken).
|
||||
tkaSyncLock sync.Mutex
|
||||
}
|
||||
|
||||
// clientGen is a func that creates a control plane client.
|
||||
@@ -355,6 +363,21 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetComponentDebugLogging gets the time that component's debug logging is
|
||||
// enabled until, or the zero time if component's time is not currently
|
||||
// enabled.
|
||||
func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
ls := b.componentLogUntil[component]
|
||||
if ls.until.IsZero() || ls.until.Before(now) {
|
||||
return time.Time{}
|
||||
}
|
||||
return ls.until
|
||||
}
|
||||
|
||||
// Dialer returns the backend's dialer.
|
||||
func (b *LocalBackend) Dialer() *tsdial.Dialer {
|
||||
return b.dialer
|
||||
@@ -775,9 +798,12 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
|
||||
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
|
||||
if err := b.tkaSyncIfNeeded(st.NetMap); err != nil {
|
||||
b.logf("[v1] TKA sync error: %v", err)
|
||||
}
|
||||
b.mu.Lock()
|
||||
|
||||
if !envknob.TKASkipSignatureCheck() {
|
||||
b.tkaFilterNetmapLocked(st.NetMap)
|
||||
}
|
||||
@@ -2321,7 +2347,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
|
||||
}
|
||||
peerAPIServices := b.peerAPIServicesLocked()
|
||||
if b.egg {
|
||||
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg"})
|
||||
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg", Port: 1})
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ type tkaState struct {
|
||||
|
||||
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
||||
// nodes from the netmap who's signature does not verify.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
if !envknob.UseWIPCode() {
|
||||
return // Feature-flag till network-lock is in Alpha.
|
||||
@@ -70,7 +72,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
nm.Peers = peers
|
||||
}
|
||||
|
||||
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
|
||||
// tkaSyncIfNeeded examines TKA info reported from the control plane,
|
||||
// performing the steps necessary to synchronize local tka state.
|
||||
//
|
||||
// There are 4 scenarios handled here:
|
||||
@@ -85,13 +87,19 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
// - Everything up to date: All other cases.
|
||||
// ∴ no action necessary.
|
||||
//
|
||||
// b.mu must be held. b.mu will be stepped out of (and back in) during network
|
||||
// RPCs.
|
||||
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
// tkaSyncIfNeeded immediately takes b.takeSyncLock which is held throughout,
|
||||
// and may take b.mu as required.
|
||||
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error {
|
||||
if !envknob.UseWIPCode() {
|
||||
// If the feature flag is not enabled, pretend we don't exist.
|
||||
return nil
|
||||
}
|
||||
|
||||
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
|
||||
defer b.tkaSyncLock.Unlock()
|
||||
b.mu.Lock() // take mu to protect access to synchronized fields.
|
||||
defer b.mu.Unlock()
|
||||
|
||||
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
|
||||
|
||||
isEnabled := b.tka != nil
|
||||
@@ -158,6 +166,8 @@ func toSyncOffer(head string, ancestors []string) (tka.SyncOffer, error) {
|
||||
// tkaSyncLocked synchronizes TKA state with control. b.mu must be held
|
||||
// and tka must be initialized. b.mu will be stepped out of (and back into)
|
||||
// during network RPCs.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
|
||||
offer, err := b.tka.authority.SyncOffer(b.tka.storage)
|
||||
if err != nil {
|
||||
|
||||
@@ -127,12 +127,10 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: a1.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
@@ -228,12 +226,10 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
|
||||
// Test that the wrong disablement secret does not shut down the authority.
|
||||
returnWrongSecret = true
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
@@ -243,12 +239,10 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
|
||||
// Test the correct disablement secret shuts down the authority.
|
||||
returnWrongSecret = false
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
@@ -468,12 +462,10 @@ func TestTKASync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Finally, lets trigger a sync.
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: controlAuthority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -232,12 +232,16 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
|
||||
if envknob.NoLogsNoSupport() {
|
||||
logMarker = "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled"
|
||||
logMarker := func() string {
|
||||
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
|
||||
}
|
||||
h.logf("user bugreport: %s", logMarker)
|
||||
if note := r.FormValue("note"); len(note) > 0 {
|
||||
if envknob.NoLogsNoSupport() {
|
||||
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
|
||||
}
|
||||
|
||||
startMarker := logMarker()
|
||||
h.logf("user bugreport: %s", startMarker)
|
||||
if note := r.URL.Query().Get("note"); len(note) > 0 {
|
||||
h.logf("user bugreport note: %s", note)
|
||||
}
|
||||
hi, _ := json.Marshal(hostinfo.New())
|
||||
@@ -247,11 +251,62 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
h.logf("user bugreport health: ok")
|
||||
}
|
||||
if defBool(r.FormValue("diagnose"), false) {
|
||||
if defBool(r.URL.Query().Get("diagnose"), false) {
|
||||
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, logMarker)
|
||||
fmt.Fprintln(w, startMarker)
|
||||
|
||||
// Nothing else to do if we're not in record mode; we wrote the marker
|
||||
// above, so we can just finish our response now.
|
||||
if !defBool(r.URL.Query().Get("record"), false) {
|
||||
return
|
||||
}
|
||||
|
||||
until := time.Now().Add(12 * time.Hour)
|
||||
|
||||
var changed map[string]bool
|
||||
for _, component := range []string{"magicsock"} {
|
||||
if h.b.GetComponentDebugLogging(component).IsZero() {
|
||||
if err := h.b.SetComponentDebugLogging(component, until); err != nil {
|
||||
h.logf("bugreport: error setting component %q logging: %v", component, err)
|
||||
continue
|
||||
}
|
||||
|
||||
mak.Set(&changed, component, true)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
for component := range changed {
|
||||
h.b.SetComponentDebugLogging(component, time.Time{})
|
||||
}
|
||||
}()
|
||||
|
||||
// NOTE(andrew): if we have anything else we want to do while recording
|
||||
// a bugreport, we can add it here.
|
||||
|
||||
// Read from the client; this will also return when the client closes
|
||||
// the connection.
|
||||
var buf [1]byte
|
||||
_, err := r.Body.Read(buf[:])
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
// good
|
||||
case errors.Is(err, io.EOF):
|
||||
// good
|
||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||
// this happens when Ctrl-C'ing the tailscale client; don't
|
||||
// bother logging an error
|
||||
default:
|
||||
// Log but continue anyway.
|
||||
h.logf("user bugreport: error reading body: %v", err)
|
||||
}
|
||||
|
||||
// Generate another log marker and return it to the client.
|
||||
endMarker := logMarker()
|
||||
h.logf("user bugreport end: %s", endMarker)
|
||||
fmt.Fprintln(w, endMarker)
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -100,7 +100,9 @@ func (c *Client) secretURL(name string) string {
|
||||
}
|
||||
|
||||
func getError(resp *http.Response) error {
|
||||
if resp.StatusCode == 200 {
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 201 {
|
||||
// These are the only success codes returned by the Kubernetes API.
|
||||
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#http-status-codes
|
||||
return nil
|
||||
}
|
||||
st := &Status{}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
// Originally a fork of interfaces_darwin.go with slightly different flags.
|
||||
// Common code for FreeBSD and Darwin. This might also work on other
|
||||
// BSD systems (e.g. OpenBSD) but has not been tested.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
@@ -37,11 +37,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
@@ -61,35 +56,20 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
if isDefaultGateway(rm) {
|
||||
return rm.Index, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -102,7 +82,7 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
@@ -112,26 +92,54 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&unix.RTF_GATEWAY == 0 {
|
||||
if !isDefaultGateway(rm) {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
|
||||
return ret, false
|
||||
}
|
||||
|
||||
var v4default = [4]byte{0, 0, 0, 0}
|
||||
var v6default = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
|
||||
func isDefaultGateway(rm *route.RouteMessage) bool {
|
||||
if rm.Flags&unix.RTF_GATEWAY == 0 {
|
||||
return false
|
||||
}
|
||||
// Defined locally because FreeBSD does not have unix.RTF_IFSCOPE.
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Addrs is [RTAX_DST, RTAX_GATEWAY, RTAX_NETMASK, ...]
|
||||
if len(rm.Addrs) <= unix.RTAX_NETMASK {
|
||||
return false
|
||||
}
|
||||
|
||||
dst := rm.Addrs[unix.RTAX_DST]
|
||||
netmask := rm.Addrs[unix.RTAX_NETMASK]
|
||||
|
||||
if dst.Family() == syscall.AF_INET &&
|
||||
netmask.Family() == syscall.AF_INET &&
|
||||
dst.(*route.Inet4Addr).IP == v4default &&
|
||||
netmask.(*route.Inet4Addr).IP == v4default {
|
||||
return true
|
||||
}
|
||||
|
||||
if dst.Family() == syscall.AF_INET6 &&
|
||||
netmask.Family() == syscall.AF_INET6 &&
|
||||
dst.(*route.Inet6Addr).IP == v6default &&
|
||||
netmask.(*route.Inet6Addr).IP == v6default {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,128 +5,16 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
)
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
d.InterfaceName = iface.Name
|
||||
d.InterfaceIndex = idx
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinFetchRIB() (ret netip.Addr, ok bool) {
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
}
|
||||
|
||||
return ret, false
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
}
|
||||
|
||||
@@ -16,18 +16,32 @@ import (
|
||||
)
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, syscallOK := likelyHomeRouterIPDarwinFetchRIB()
|
||||
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
syscallIP, syscallOK := likelyHomeRouterIPBSDFetchRIB()
|
||||
netstatIP, netstatIf, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
|
||||
if !syscallOK {
|
||||
return
|
||||
}
|
||||
|
||||
def, err := defaultRoute()
|
||||
if err != nil {
|
||||
t.Errorf("defaultRoute() error: %v", err)
|
||||
}
|
||||
|
||||
if def.InterfaceName != netstatIf {
|
||||
t.Errorf("syscall default route interface %s differs from netstat %s", def.InterfaceName, netstatIf)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
Parse out 10.0.0.1 and en0 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
@@ -40,12 +54,12 @@ default link#14 UCSI utun2
|
||||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
*/
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
|
||||
if version.IsMobile() {
|
||||
// Don't try to do subprocesses on iOS. Ends up with log spam like:
|
||||
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
|
||||
// This is why we have likelyHomeRouterIPDarwinSyscall.
|
||||
return ret, false
|
||||
return ret, "", false
|
||||
}
|
||||
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
@@ -64,22 +78,26 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("default") {
|
||||
if len(f) < 4 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm := f[1], f[2]
|
||||
ipm, flagsm, netifm := f[1], f[2], f[3]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
if mem.Contains(flagsm, mem.S("I")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
|
||||
if err == nil && ip.IsPrivate() {
|
||||
ret = ip
|
||||
netif = netifm.StringCopy()
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, ret.IsValid()
|
||||
return ret, netif, ret.IsValid()
|
||||
}
|
||||
|
||||
func TestFetchRoutingTable(t *testing.T) {
|
||||
|
||||
26
net/interfaces/interfaces_freebsd.go
Normal file
26
net/interfaces/interfaces_freebsd.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
|
||||
}
|
||||
@@ -1105,6 +1105,9 @@ func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, pr
|
||||
}
|
||||
rids = append(rids, id)
|
||||
}
|
||||
if len(rids) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
preferredDERP = rids[rand.Intn(len(rids))]
|
||||
}
|
||||
|
||||
@@ -1113,13 +1116,20 @@ func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, pr
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
chal := "tailscale " + node.HostName
|
||||
req.Header.Set("X-Tailscale-Challenge", chal)
|
||||
r, err := noRedirectClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
c.logf("[v2] checkCaptivePortal url=%q status_code=%d", req.URL.String(), r.StatusCode)
|
||||
defer r.Body.Close()
|
||||
|
||||
return r.StatusCode != 204, nil
|
||||
expectedResponse := "response " + chal
|
||||
validResponse := r.Header.Get("X-Tailscale-Response") == expectedResponse
|
||||
|
||||
c.logf("[v2] checkCaptivePortal url=%q status_code=%d valid_response=%v", req.URL.String(), r.StatusCode, validResponse)
|
||||
return r.StatusCode != 204 || !validResponse, nil
|
||||
}
|
||||
|
||||
// runHTTPOnlyChecks is the netcheck done by environments that can
|
||||
|
||||
213
net/wsconn/wsconn.go
Normal file
213
net/wsconn/wsconn.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package wsconn contains an adapter type that turns
|
||||
// a websocket connection into a net.Conn. It a temporary fork of the
|
||||
// netconn.go file from the nhooyr.io/websocket package while we wait for
|
||||
// https://github.com/nhooyr/websocket/pull/350 to be merged.
|
||||
package wsconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
// NetConn converts a *websocket.Conn into a net.Conn.
|
||||
//
|
||||
// It's for tunneling arbitrary protocols over WebSockets.
|
||||
// Few users of the library will need this but it's tricky to implement
|
||||
// correctly and so provided in the library.
|
||||
// See https://github.com/nhooyr/websocket/issues/100.
|
||||
//
|
||||
// Every Write to the net.Conn will correspond to a message write of
|
||||
// the given type on *websocket.Conn.
|
||||
//
|
||||
// The passed ctx bounds the lifetime of the net.Conn. If cancelled,
|
||||
// all reads and writes on the net.Conn will be cancelled.
|
||||
//
|
||||
// If a message is read that is not of the correct type, the connection
|
||||
// will be closed with StatusUnsupportedData and an error will be returned.
|
||||
//
|
||||
// Close will close the *websocket.Conn with StatusNormalClosure.
|
||||
//
|
||||
// When a deadline is hit, the connection will be closed. This is
|
||||
// different from most net.Conn implementations where only the
|
||||
// reading/writing goroutines are interrupted but the connection is kept alive.
|
||||
//
|
||||
// The Addr methods will return a mock net.Addr that returns "websocket" for Network
|
||||
// and "websocket/unknown-addr" for String.
|
||||
//
|
||||
// A received StatusNormalClosure or StatusGoingAway close frame will be translated to
|
||||
// io.EOF when reading.
|
||||
func NetConn(ctx context.Context, c *websocket.Conn, msgType websocket.MessageType) net.Conn {
|
||||
nc := &netConn{
|
||||
c: c,
|
||||
msgType: msgType,
|
||||
}
|
||||
|
||||
var writeCancel context.CancelFunc
|
||||
nc.writeContext, writeCancel = context.WithCancel(ctx)
|
||||
nc.writeTimer = time.AfterFunc(math.MaxInt64, func() {
|
||||
nc.afterWriteDeadline.Store(true)
|
||||
if nc.writing.Load() {
|
||||
writeCancel()
|
||||
}
|
||||
})
|
||||
if !nc.writeTimer.Stop() {
|
||||
<-nc.writeTimer.C
|
||||
}
|
||||
|
||||
var readCancel context.CancelFunc
|
||||
nc.readContext, readCancel = context.WithCancel(ctx)
|
||||
nc.readTimer = time.AfterFunc(math.MaxInt64, func() {
|
||||
nc.afterReadDeadline.Store(true)
|
||||
if nc.reading.Load() {
|
||||
readCancel()
|
||||
}
|
||||
})
|
||||
if !nc.readTimer.Stop() {
|
||||
<-nc.readTimer.C
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
type netConn struct {
|
||||
c *websocket.Conn
|
||||
msgType websocket.MessageType
|
||||
|
||||
writeTimer *time.Timer
|
||||
writeContext context.Context
|
||||
writing atomic.Bool
|
||||
afterWriteDeadline atomic.Bool
|
||||
|
||||
readTimer *time.Timer
|
||||
readContext context.Context
|
||||
reading atomic.Bool
|
||||
afterReadDeadline atomic.Bool
|
||||
|
||||
readMu sync.Mutex
|
||||
eofed bool
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
var _ net.Conn = &netConn{}
|
||||
|
||||
func (c *netConn) Close() error {
|
||||
return c.c.Close(websocket.StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
func (c *netConn) Write(p []byte) (int, error) {
|
||||
if c.afterWriteDeadline.Load() {
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
}
|
||||
|
||||
if swapped := c.writing.CompareAndSwap(false, true); !swapped {
|
||||
panic("Concurrent writes not allowed")
|
||||
}
|
||||
defer c.writing.Store(false)
|
||||
|
||||
err := c.c.Write(c.writeContext, c.msgType, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *netConn) Read(p []byte) (int, error) {
|
||||
if c.afterReadDeadline.Load() {
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
}
|
||||
|
||||
c.readMu.Lock()
|
||||
defer c.readMu.Unlock()
|
||||
if swapped := c.reading.CompareAndSwap(false, true); !swapped {
|
||||
panic("Concurrent reads not allowed")
|
||||
}
|
||||
defer c.reading.Store(false)
|
||||
|
||||
if c.eofed {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if c.reader == nil {
|
||||
typ, r, err := c.c.Reader(c.readContext)
|
||||
if err != nil {
|
||||
switch websocket.CloseStatus(err) {
|
||||
case websocket.StatusNormalClosure, websocket.StatusGoingAway:
|
||||
c.eofed = true
|
||||
return 0, io.EOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if typ != c.msgType {
|
||||
err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ)
|
||||
c.c.Close(websocket.StatusUnsupportedData, err.Error())
|
||||
return 0, err
|
||||
}
|
||||
c.reader = r
|
||||
}
|
||||
|
||||
n, err := c.reader.Read(p)
|
||||
if err == io.EOF {
|
||||
c.reader = nil
|
||||
err = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type websocketAddr struct {
|
||||
}
|
||||
|
||||
func (a websocketAddr) Network() string {
|
||||
return "websocket"
|
||||
}
|
||||
|
||||
func (a websocketAddr) String() string {
|
||||
return "websocket/unknown-addr"
|
||||
}
|
||||
|
||||
func (c *netConn) RemoteAddr() net.Addr {
|
||||
return websocketAddr{}
|
||||
}
|
||||
|
||||
func (c *netConn) LocalAddr() net.Addr {
|
||||
return websocketAddr{}
|
||||
}
|
||||
|
||||
func (c *netConn) SetDeadline(t time.Time) error {
|
||||
c.SetWriteDeadline(t)
|
||||
c.SetReadDeadline(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *netConn) SetWriteDeadline(t time.Time) error {
|
||||
if t.IsZero() {
|
||||
c.writeTimer.Stop()
|
||||
} else {
|
||||
c.writeTimer.Reset(time.Until(t))
|
||||
}
|
||||
c.afterWriteDeadline.Store(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *netConn) SetReadDeadline(t time.Time) error {
|
||||
if t.IsZero() {
|
||||
c.readTimer.Stop()
|
||||
} else {
|
||||
c.readTimer.Reset(time.Until(t))
|
||||
}
|
||||
c.afterReadDeadline.Store(false)
|
||||
return nil
|
||||
}
|
||||
@@ -29,13 +29,20 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if *addr == ":443" {
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
})
|
||||
}
|
||||
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||
who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
|
||||
@@ -216,6 +216,17 @@ func (v IPPrefixSlice) ContainsExitRoutes() bool {
|
||||
return tsaddr.ContainsExitRoutes(v.ж.ж)
|
||||
}
|
||||
|
||||
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
|
||||
// Routes other than ExitNode Routes.
|
||||
func (v IPPrefixSlice) ContainsNonExitSubnetRoutes() bool {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if v.At(i).Bits() != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (v IPPrefixSlice) MarshalJSON() ([]byte, error) {
|
||||
return v.ж.MarshalJSON()
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/wgengine/winnet"
|
||||
)
|
||||
@@ -247,7 +248,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
|
||||
}
|
||||
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
const mtu = 0
|
||||
const mtu = tstun.DefaultMTU
|
||||
luid := winipcfg.LUID(tun.LUID())
|
||||
iface, err := interfaceFromLUID(luid,
|
||||
// Issue 474: on early boot, when the network is still
|
||||
|
||||
@@ -1532,25 +1532,10 @@ func cidrDiff(kind string, old map[netip.Prefix]bool, new []netip.Prefix, add, d
|
||||
ret[cidr] = true
|
||||
}
|
||||
|
||||
var delFail []error
|
||||
for cidr := range old {
|
||||
if newMap[cidr] {
|
||||
continue
|
||||
}
|
||||
if err := del(cidr); err != nil {
|
||||
logf("%s del failed: %v", kind, err)
|
||||
delFail = append(delFail, err)
|
||||
} else {
|
||||
delete(ret, cidr)
|
||||
}
|
||||
}
|
||||
if len(delFail) == 1 {
|
||||
return ret, delFail[0]
|
||||
}
|
||||
if len(delFail) > 0 {
|
||||
return ret, fmt.Errorf("%d delete %s failures; first was: %w", len(delFail), kind, delFail[0])
|
||||
}
|
||||
|
||||
// We want to add before we delete, so that if there is no overlap, we don't
|
||||
// end up in a state where we have no addresses on an interface as that
|
||||
// results in other kernel entities (like routes) pointing to that interface
|
||||
// to also be deleted.
|
||||
var addFail []error
|
||||
for cidr := range newMap {
|
||||
if old[cidr] {
|
||||
@@ -1571,6 +1556,25 @@ func cidrDiff(kind string, old map[netip.Prefix]bool, new []netip.Prefix, add, d
|
||||
return ret, fmt.Errorf("%d add %s failures; first was: %w", len(addFail), kind, addFail[0])
|
||||
}
|
||||
|
||||
var delFail []error
|
||||
for cidr := range old {
|
||||
if newMap[cidr] {
|
||||
continue
|
||||
}
|
||||
if err := del(cidr); err != nil {
|
||||
logf("%s del failed: %v", kind, err)
|
||||
delFail = append(delFail, err)
|
||||
} else {
|
||||
delete(ret, cidr)
|
||||
}
|
||||
}
|
||||
if len(delFail) == 1 {
|
||||
return ret, delFail[0]
|
||||
}
|
||||
if len(delFail) > 0 {
|
||||
return ret, fmt.Errorf("%d delete %s failures; first was: %w", len(delFail), kind, delFail[0])
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -839,3 +841,84 @@ Usage: busybox [function [arguments]...]
|
||||
t.Errorf("version = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDRDiff(t *testing.T) {
|
||||
pfx := func(p ...string) []netip.Prefix {
|
||||
var ret []netip.Prefix
|
||||
for _, s := range p {
|
||||
ret = append(ret, netip.MustParsePrefix(s))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
tests := []struct {
|
||||
old []netip.Prefix
|
||||
new []netip.Prefix
|
||||
wantAdd []netip.Prefix
|
||||
wantDel []netip.Prefix
|
||||
final []netip.Prefix
|
||||
}{
|
||||
{
|
||||
old: nil,
|
||||
new: pfx("1.1.1.1/32"),
|
||||
wantAdd: pfx("1.1.1.1/32"),
|
||||
final: pfx("1.1.1.1/32"),
|
||||
},
|
||||
{
|
||||
old: pfx("1.1.1.1/32"),
|
||||
new: pfx("1.1.1.1/32"),
|
||||
final: pfx("1.1.1.1/32"),
|
||||
},
|
||||
{
|
||||
old: pfx("1.1.1.1/32", "2.3.4.5/32"),
|
||||
new: pfx("1.1.1.1/32"),
|
||||
wantDel: pfx("2.3.4.5/32"),
|
||||
final: pfx("1.1.1.1/32"),
|
||||
},
|
||||
{
|
||||
old: pfx("1.1.1.1/32", "2.3.4.5/32"),
|
||||
new: pfx("1.0.0.0/32", "3.4.5.6/32"),
|
||||
wantDel: pfx("1.1.1.1/32", "2.3.4.5/32"),
|
||||
wantAdd: pfx("1.0.0.0/32", "3.4.5.6/32"),
|
||||
final: pfx("1.0.0.0/32", "3.4.5.6/32"),
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
om := make(map[netip.Prefix]bool)
|
||||
for _, p := range tc.old {
|
||||
om[p] = true
|
||||
}
|
||||
var added []netip.Prefix
|
||||
var deleted []netip.Prefix
|
||||
fm, err := cidrDiff("test", om, tc.new, func(p netip.Prefix) error {
|
||||
if len(deleted) > 0 {
|
||||
t.Error("delete called before add")
|
||||
}
|
||||
added = append(added, p)
|
||||
return nil
|
||||
}, func(p netip.Prefix) error {
|
||||
deleted = append(deleted, p)
|
||||
return nil
|
||||
}, t.Logf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.SortFunc(added, func(a, b netip.Prefix) bool { return a.Addr().Less(b.Addr()) })
|
||||
slices.SortFunc(deleted, func(a, b netip.Prefix) bool { return a.Addr().Less(b.Addr()) })
|
||||
if !reflect.DeepEqual(added, tc.wantAdd) {
|
||||
t.Errorf("added = %v, want %v", added, tc.wantAdd)
|
||||
}
|
||||
if !reflect.DeepEqual(deleted, tc.wantDel) {
|
||||
t.Errorf("deleted = %v, want %v", deleted, tc.wantDel)
|
||||
}
|
||||
|
||||
// Check that the final state is correct.
|
||||
if len(fm) != len(tc.final) {
|
||||
t.Fatalf("final state = %v, want %v", fm, tc.final)
|
||||
}
|
||||
for _, p := range tc.final {
|
||||
if !fm[p] {
|
||||
t.Errorf("final state = %v, want %v", fm, tc.final)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ type Engine interface {
|
||||
Reconfig(*wgcfg.Config, *router.Config, *dns.Config, *tailcfg.Debug) error
|
||||
|
||||
// PeerForIP returns the node to which the provided IP routes,
|
||||
// if any. If none is found, (nil, nil) is returned.
|
||||
// if any. If none is found, (nil, false) is returned.
|
||||
PeerForIP(netip.Addr) (_ PeerForIP, ok bool)
|
||||
|
||||
// GetFilter returns the current packet filter, if any.
|
||||
|
||||
Reference in New Issue
Block a user