diff --git a/agent/agent.go b/agent/agent.go
index 52c423787fb44..e435b795d5fc9 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -22,6 +22,7 @@ import (
"time"
"github.com/armon/circbuf"
+ "github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
@@ -1408,24 +1409,57 @@ func (a *agent) isClosed() bool {
}
func (a *agent) HTTPDebug() http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ r := chi.NewRouter()
+
+ requireNetwork := func(w http.ResponseWriter) (*tailnet.Conn, bool) {
a.closeMutex.Lock()
network := a.network
a.closeMutex.Unlock()
if network == nil {
- w.WriteHeader(http.StatusOK)
+ w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("network is not ready yet"))
+ return nil, false
+ }
+
+ return network, true
+ }
+
+ r.Get("/debug/magicsock", func(w http.ResponseWriter, r *http.Request) {
+ network, ok := requireNetwork(w)
+ if !ok {
return
}
+ network.MagicsockServeHTTPDebug(w, r)
+ })
- if r.URL.Path == "/debug/magicsock" {
- network.MagicsockServeHTTPDebug(w, r)
- } else {
- w.WriteHeader(http.StatusNotFound)
- _, _ = w.Write([]byte("404 not found"))
+ r.Get("/debug/magicsock/debug-logging/{state}", func(w http.ResponseWriter, r *http.Request) {
+ state := chi.URLParam(r, "state")
+ stateBool, err := strconv.ParseBool(state)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = fmt.Fprintf(w, "invalid state %q, must be a boolean", state)
+ return
+ }
+
+ network, ok := requireNetwork(w)
+ if !ok {
+ return
}
+
+ network.MagicsockSetDebugLoggingEnabled(stateBool)
+ a.logger.Info(r.Context(), "updated magicsock debug logging due to debug request", slog.F("new_state", stateBool))
+
+ w.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprintf(w, "updated magicsock debug logging to %v", stateBool)
})
+
+ r.NotFound(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte("404 not found"))
+ })
+
+ return r
}
func (a *agent) Close() error {
diff --git a/agent/agent_test.go b/agent/agent_test.go
index 34637992536b7..92be40764f209 100644
--- a/agent/agent_test.go
+++ b/agent/agent_test.go
@@ -1932,6 +1932,96 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
}, testutil.WaitShort, testutil.IntervalFast)
}
+func TestAgent_DebugServer(t *testing.T) {
+ t.Parallel()
+
+ derpMap, _ := tailnettest.RunDERPAndSTUN(t)
+ //nolint:dogsled
+ conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{
+ DERPMap: derpMap,
+ }, 0)
+
+ awaitReachableCtx := testutil.Context(t, testutil.WaitLong)
+ ok := conn.AwaitReachable(awaitReachableCtx)
+ require.True(t, ok)
+ _ = conn.Close()
+
+ srv := httptest.NewServer(agnt.HTTPDebug())
+ t.Cleanup(srv.Close)
+
+ t.Run("MagicsockDebug", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), "
magicsock
")
+ })
+
+ t.Run("MagicsockDebugLogging", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Enable", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/t", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), "updated magicsock debug logging to true")
+ })
+
+ t.Run("Disable", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/0", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), "updated magicsock debug logging to false")
+ })
+
+ t.Run("Invalid", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/blah", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusBadRequest, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`)
+ })
+ })
+}
+
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) (*ptytest.PTYCmd, pty.Process) {
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
@@ -2013,7 +2103,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati
*agenttest.Client,
<-chan *agentsdk.Stats,
afero.Fs,
- io.Closer,
+ agent.Agent,
) {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
if metadata.DERPMap == nil {
diff --git a/tailnet/conn.go b/tailnet/conn.go
index 089a83ff79ff9..ebe57d2606b1c 100644
--- a/tailnet/conn.go
+++ b/tailnet/conn.go
@@ -8,6 +8,7 @@ import (
"net"
"net/http"
"net/netip"
+ "os"
"reflect"
"strconv"
"sync"
@@ -51,6 +52,14 @@ const (
WorkspaceAgentSpeedtestPort = 3
)
+// EnvMagicsockDebugLogging enables super-verbose logging for the magicsock
+// internals. A logger must be supplied to the connection with the debug level
+// enabled.
+//
+// With this disabled, you still get a lot of output if you have a valid logger
+// with the debug level enabled.
+const EnvMagicsockDebugLogging = "CODER_MAGICSOCK_DEBUG_LOGGING"
+
func init() {
// Globally disable network namespacing. All networking happens in
// userspace.
@@ -175,6 +184,18 @@ func NewConn(options *Options) (conn *Conn, err error) {
magicConn.SetDERPHeader(options.DERPHeader.Clone())
}
+ if v, ok := os.LookupEnv(EnvMagicsockDebugLogging); ok {
+ vBool, err := strconv.ParseBool(v)
+ if err != nil {
+ options.Logger.Debug(context.Background(), fmt.Sprintf("magicsock debug logging disabled due to invalid value %s=%q, use true or false", EnvMagicsockDebugLogging, v))
+ } else {
+ magicConn.SetDebugLoggingEnabled(vBool)
+ options.Logger.Debug(context.Background(), fmt.Sprintf("magicsock debug logging set by %s=%t", EnvMagicsockDebugLogging, vBool))
+ }
+ } else {
+ options.Logger.Debug(context.Background(), fmt.Sprintf("magicsock debug logging disabled, use %s=true to enable", EnvMagicsockDebugLogging))
+ }
+
// Update the keys for the magic connection!
err = magicConn.SetPrivateKey(nodePrivateKey)
if err != nil {
@@ -361,6 +382,10 @@ type Conn struct {
trafficStats *connstats.Statistics
}
+func (c *Conn) MagicsockSetDebugLoggingEnabled(enabled bool) {
+ c.magicConn.SetDebugLoggingEnabled(enabled)
+}
+
func (c *Conn) SetAddresses(ips []netip.Prefix) error {
c.mutex.Lock()
defer c.mutex.Unlock()