tailscale/ipn/localapi/debugderp.go

305 lines
8.8 KiB
Go
Raw Permalink Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package localapi
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"strconv"
"time"
"tailscale.com/derp/derphttp"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netaddr"
"tailscale.com/net/netns"
"tailscale.com/net/stun"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/nettype"
)
func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
var st ipnstate.DebugDERPRegionReport
defer func() {
j, _ := json.Marshal(st)
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}()
dm := h.b.DERPMap()
if dm == nil {
st.Errors = append(st.Errors, "no DERP map (not connected?)")
return
}
regStr := r.FormValue("region")
var reg *tailcfg.DERPRegion
if id, err := strconv.Atoi(regStr); err == nil {
reg = dm.Regions[id]
} else {
for _, r := range dm.Regions {
if r.RegionCode == regStr {
reg = r
break
}
}
}
if reg == nil {
st.Errors = append(st.Errors, fmt.Sprintf("no such region %q in DERP map", regStr))
return
}
st.Info = append(st.Info, fmt.Sprintf("Region %v == %q", reg.RegionID, reg.RegionCode))
if len(dm.Regions) == 1 {
st.Warnings = append(st.Warnings, "Having only a single DERP region (i.e. removing the default Tailscale-provided regions) is a single point of failure and could hamper connectivity")
}
if reg.Avoid {
st.Warnings = append(st.Warnings, "Region is marked with Avoid bit")
}
if len(reg.Nodes) == 0 {
st.Errors = append(st.Errors, "Region has no nodes defined")
return
}
ctx := r.Context()
var (
dialer net.Dialer
client *http.Client = http.DefaultClient
)
checkConn := func(derpNode *tailcfg.DERPNode) bool {
port := firstNonzero(derpNode.DERPPort, 443)
var (
hasIPv4 bool
hasIPv6 bool
)
// Check IPv4 first
addr := net.JoinHostPort(firstNonzero(derpNode.IPv4, derpNode.HostName), strconv.Itoa(port))
conn, err := dialer.DialContext(ctx, "tcp4", addr)
if err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ %q over IPv4: %v", derpNode.HostName, addr, err))
} else {
defer conn.Close()
// Upgrade to TLS and verify that works properly.
tlsConn := tls.Client(conn, &tls.Config{
ServerName: firstNonzero(derpNode.CertName, derpNode.HostName),
})
if err := tlsConn.HandshakeContext(ctx); err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error upgrading connection to node %q @ %q to TLS over IPv4: %v", derpNode.HostName, addr, err))
} else {
hasIPv4 = true
}
}
// Check IPv6
addr = net.JoinHostPort(firstNonzero(derpNode.IPv6, derpNode.HostName), strconv.Itoa(port))
conn, err = dialer.DialContext(ctx, "tcp6", addr)
if err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ %q over IPv6: %v", derpNode.HostName, addr, err))
} else {
defer conn.Close()
// Upgrade to TLS and verify that works properly.
tlsConn := tls.Client(conn, &tls.Config{
ServerName: firstNonzero(derpNode.CertName, derpNode.HostName),
// TODO(andrew-d): we should print more
// detailed failure information on if/why TLS
// verification fails
})
if err := tlsConn.HandshakeContext(ctx); err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error upgrading connection to node %q @ %q to TLS over IPv6: %v", derpNode.HostName, addr, err))
} else {
hasIPv6 = true
}
}
// If we only have an IPv6 conn, then warn; we want both.
if hasIPv6 && !hasIPv4 {
st.Warnings = append(st.Warnings, fmt.Sprintf("Node %q only has IPv6 connectivity, not IPv4", derpNode.HostName))
} else if hasIPv6 && hasIPv4 {
st.Info = append(st.Info, fmt.Sprintf("Node %q has working IPv4 and IPv6 connectivity", derpNode.HostName))
}
return hasIPv4 || hasIPv6
}
checkSTUN4 := func(derpNode *tailcfg.DERPNode) {
u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(h.logf, h.netMon)).ListenPacket(ctx, "udp4", ":0")
if err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error creating IPv4 STUN listener: %v", err))
return
}
defer u4.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var addr netip.Addr
if derpNode.IPv4 != "" {
addr, err = netip.ParseAddr(derpNode.IPv4)
if err != nil {
// Error printed elsewhere
return
}
} else {
addrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", derpNode.HostName)
if err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error resolving node %q IPv4 addresses: %v", derpNode.HostName, err))
return
}
addr = addrs[0]
}
addrPort := netip.AddrPortFrom(addr, uint16(firstNonzero(derpNode.STUNPort, 3478)))
txID := stun.NewTxID()
req := stun.Request(txID)
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-ctx.Done():
case <-done:
}
u4.Close()
}()
gotResponse := make(chan netip.AddrPort, 1)
go func() {
defer u4.Close()
var buf [64 << 10]byte
for {
n, addr, err := u4.ReadFromUDPAddrPort(buf[:])
if err != nil {
return
}
pkt := buf[:n]
if !stun.Is(pkt) {
continue
}
ap := netaddr.Unmap(addr)
if !ap.IsValid() {
continue
}
tx, addrPort, err := stun.ParseResponse(pkt)
if err != nil {
continue
}
if tx == txID {
gotResponse <- addrPort
return
}
}
}()
_, err = u4.WriteToUDPAddrPort(req, addrPort)
if err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error sending IPv4 STUN packet to %v (%q): %v", addrPort, derpNode.HostName, err))
return
}
select {
case resp := <-gotResponse:
st.Info = append(st.Info, fmt.Sprintf("Node %q returned IPv4 STUN response: %v", derpNode.HostName, resp))
case <-ctx.Done():
st.Warnings = append(st.Warnings, fmt.Sprintf("Node %q did not return a IPv4 STUN response", derpNode.HostName))
}
}
// Start by checking whether we can establish a HTTP connection
for _, derpNode := range reg.Nodes {
connSuccess := checkConn(derpNode)
// Verify that the /generate_204 endpoint works
captivePortalURL := "http://" + derpNode.HostName + "/generate_204"
resp, err := client.Get(captivePortalURL)
if err != nil {
st.Warnings = append(st.Warnings, fmt.Sprintf("Error making request to the captive portal check %q; is port 80 blocked?", captivePortalURL))
} else {
resp.Body.Close()
}
if !connSuccess {
continue
}
fakePrivKey := key.NewNode()
// Next, repeatedly get the server key to see if the node is
// behind a load balancer (incorrectly).
serverPubKeys := make(map[key.NodePublic]bool)
for i := 0; i < 5; i++ {
func() {
rc := derphttp.NewRegionClient(fakePrivKey, h.logf, h.netMon, func() *tailcfg.DERPRegion {
return &tailcfg.DERPRegion{
RegionID: reg.RegionID,
RegionCode: reg.RegionCode,
RegionName: reg.RegionName,
Nodes: []*tailcfg.DERPNode{derpNode},
}
})
if err := rc.Connect(ctx); err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ try %d: %v", derpNode.HostName, i, err))
return
}
if len(serverPubKeys) == 0 {
st.Info = append(st.Info, fmt.Sprintf("Successfully established a DERP connection with node %q", derpNode.HostName))
}
serverPubKeys[rc.ServerPublicKey()] = true
}()
}
if len(serverPubKeys) > 1 {
st.Errors = append(st.Errors, fmt.Sprintf("Received multiple server public keys (%d); is the DERP server behind a load balancer?", len(serverPubKeys)))
}
// Send a STUN query to this node to verify whether or not it
// correctly returns an IP address.
checkSTUN4(derpNode)
}
// TODO(bradfitz): finish:
// * try to DERP auth with new public key.
// * if rejected, add Info that it's likely the DERP server authz is on,
// try with LocalBackend's node key instead.
// * if they have more then one node, try to relay a packet between them
// and see if it works (like cmd/derpprobe). But if server authz is on,
// we won't be able to, so just warn. Say to turn that off, try again,
// then turn it back on. TODO(bradfitz): maybe add a debug frame to DERP
// protocol to say how many peers it's meshed with. Should match count
// in DERPRegion. Or maybe even list all their server pub keys that it's peered
// with.
// * If their certificate is bad, either expired or just wrongly
// issued in the first place, tell them specifically that the
// cert is bad not just that the connection failed.
}
func firstNonzero[T comparable](items ...T) T {
var zero T
for _, item := range items {
if item != zero {
return item
}
}
return zero
}