cmd/tailscale, net/portmapper: add --log-http option to "debug portmap"

This option allows logging the raw HTTP requests and responses that the
portmapper Client makes when using UPnP. This can be extremely helpful
when debugging strange UPnP issues with users' devices, and might allow
us to avoid having to instruct users to perform a packet capture.

Updates #8992

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I2c3cf6930b09717028deaff31738484cc9b008e4
This commit is contained in:
Andrew Dunham
2023-08-21 16:53:47 -04:00
parent 3451b89e5f
commit c86a610eb3
5 changed files with 142 additions and 14 deletions

View File

@@ -36,6 +36,11 @@ type DebugKnobs struct {
// to its logger.
VerboseLogs bool
// LogHTTP tells the Client to print the raw HTTP logs (from UPnP) to
// its logger. This is useful when debugging buggy UPnP
// implementations.
LogHTTP bool
// Disable* disables a specific service from mapping.
DisableUPnP bool
DisablePMP bool

View File

@@ -12,12 +12,14 @@ import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"sync/atomic"
"time"
"github.com/tailscale/goupnp"
@@ -261,6 +263,9 @@ func (c *Client) upnpHTTPClientLocked() *http.Client {
IdleConnTimeout: 2 * time.Second, // LAN is cheap
},
}
if c.debug.LogHTTP {
c.uPnPHTTPClient = requestLogger(c.logf, c.uPnPHTTPClient)
}
}
return c.uPnPHTTPClient
}
@@ -369,3 +374,60 @@ func parseUPnPDiscoResponse(body []byte) (uPnPDiscoResponse, error) {
r.USN = res.Header.Get("Usn")
return r, nil
}
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (r roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
func requestLogger(logf logger.Logf, client *http.Client) *http.Client {
// Clone the HTTP client, and override the Transport to log to the
// provided logger.
ret := *client
oldTransport := ret.Transport
var requestCounter atomic.Uint64
loggingTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
ctr := requestCounter.Add(1)
// Read the body and re-set it.
var (
body []byte
err error
)
if req.Body != nil {
body, err = io.ReadAll(req.Body)
req.Body.Close()
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
}
logf("request[%d]: %s %q body=%q", ctr, req.Method, req.URL, body)
resp, err := oldTransport.RoundTrip(req)
if err != nil {
logf("response[%d]: err=%v", err)
return nil, err
}
// Read the response body
if resp.Body != nil {
body, err = io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
logf("response[%d]: %d bodyErr=%v", resp.StatusCode, err)
return nil, err
}
resp.Body = io.NopCloser(bytes.NewReader(body))
}
logf("response[%d]: %d body=%q", ctr, resp.StatusCode, body)
return resp, nil
})
ret.Transport = loggingTransport
return &ret
}