{"record":{"id":"2a587a034b561f6e","repo":"tailscale/tailscale","slug":"failed-hijacking-conn","errorCode":null,"errorMessage":"failed hijacking conn","messagePattern":"failed hijacking conn","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"ipn/ipnlocal/serve.go","lineNumber":1528,"sourceCode":"\tif err != nil {\n\t\tbad(\"Tailscale-Ingress-Src header invalid; want ip:port\")\n\t\treturn\n\t}\n\ttarget := ipn.HostPort(r.Header.Get(\"Tailscale-Ingress-Target\"))\n\tif target == \"\" {\n\t\tbad(\"Tailscale-Ingress-Target header not set\")\n\t\treturn\n\t}\n\tif _, _, err := net.SplitHostPort(string(target)); err != nil {\n\t\tbad(\"Tailscale-Ingress-Target header invalid; want host:port\")\n\t\treturn\n\t}\n\n\tgetConnOrReset := func() (net.Conn, bool) {\n\t\tconn, _, err := w.(http.Hijacker).Hijack()\n\t\tif err != nil {\n\t\t\th.logf(\"ingress: failed hijacking conn\")\n\t\t\thttp.Error(w, \"failed hijacking conn\", http.StatusInternalServerError)\n\t\t\treturn nil, false\n\t\t}\n\t\tio.WriteString(conn, \"HTTP/1.1 101 Switching Protocols\\r\\n\\r\\n\")\n\t\treturn &ipn.FunnelConn{\n\t\t\tConn:   conn,\n\t\t\tSrc:    srcAddr,\n\t\t\tTarget: target,\n\t\t}, true\n\t}\n\tsendRST := func() {\n\t\thttp.Error(w, \"denied\", http.StatusForbidden)\n\t}\n\n\th.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)\n}\n\n// wantIngressLocked reports whether this node has ingress configured. This bool\n// is sent to the coordination server (in Hostinfo.WireIngress) as an","sourceCodeStart":1510,"sourceCodeEnd":1546,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/ipn/ipnlocal/serve.go#L1510-L1546","documentation":"To bridge a funnel TLS connection, the handler hijacks the underlying TCP conn from the HTTP server via w.(http.Hijacker).Hijack(). If the ResponseWriter does not implement Hijacker or the hijack call fails, the handler returns 500 'failed hijacking conn' and logs it.","triggerScenarios":"The peerapi HTTP server is wrapped in middleware that replaces ResponseWriter with a non-Hijacker type (custom builds or embedding), or the connection is already torn down when Hijack runs.","commonSituations":"tsnet or custom servers inserting observability or wrapping middleware in front of the peerapi mux; essentially never happens in stock tailscaled.","solutions":["Remove or fix ResponseWriter wrappers so the Hijacker interface propagates (delegate the http.Hijacker methods in wrappers).","Reproduce with stock tailscaled to confirm the wrapper is the cause."],"exampleFix":"// before: middleware wrapper hides optional interfaces\ntype wrap struct{ http.ResponseWriter }\n\n// after: forward Hijacker so ingress can take over the conn\ntype wrap struct{ http.ResponseWriter }\n\nfunc (w *wrap) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n    hj, ok := w.ResponseWriter.(http.Hijacker)\n    if !ok {\n        return nil, nil, fmt.Errorf(\"underlying writer is not a Hijacker\")\n    }\n    return hj.Hijack()\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Middleware must not break ingress hijacking:\nfunc hijackable(w http.ResponseWriter) bool {\n    _, ok := w.(http.Hijacker)\n    return ok\n}","tryCatchPattern":null,"preventionTips":["In any ResponseWriter middleware, explicitly forward http.Hijacker (and http.Flusher) methods.","Test funnel paths after adding HTTP middleware in custom builds."],"tags":["funnel","ingress","tailscale","http","hijack","go"],"backgroundTag":"http-hijack-failed","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}