diff --git a/ipn/localapi/debugderp.go b/ipn/localapi/debugderp.go index 2db3e4f2f..f7ae88787 100644 --- a/ipn/localapi/debugderp.go +++ b/ipn/localapi/debugderp.go @@ -4,17 +4,24 @@ 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) { @@ -132,6 +139,92 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) { return hasIPv4 || hasIPv6 } + checkSTUN4 := func(derpNode *tailcfg.DERPNode) { + u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(h.logf)).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) @@ -178,6 +271,10 @@ func() { 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: @@ -191,7 +288,6 @@ func() { // 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. - // * try STUN queries // * 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.