fix(handler/http2): apply traffic limiter + stats to forward proxy path, decouple auth I/O, add idle timeout

- Wrap non-CONNECT upstream with traffic_wrapper and stats_wrapper (was only on CONNECT path)
- Write 503 error response on forwardRequest failure instead of silent connection close
- Decouple resp.Header from w.Header() to prevent metadata header leakage/doubling in 407 responses
- Return pipeTo signal from authenticate instead of dialing inline (matches http handler pattern)
- Add idleTimeout metadata field and pass to xnet.Pipe for CONNECT tunnels
- Add 4 tests for probe resistance host forwarding and knock bypass
This commit is contained in:
ginuerzh
2026-05-30 20:06:56 +08:00
parent 6ccaae0573
commit e543c0b0fd
6 changed files with 144 additions and 30 deletions
+43 -6
View File
@@ -86,7 +86,7 @@ func TestAuthenticate(t *testing.T) {
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
id, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
id, ok, _ := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if !ok {
t.Error("expected ok without auther")
}
@@ -102,7 +102,7 @@ func TestAuthenticate(t *testing.T) {
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
id, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
id, ok, _ := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if !ok {
t.Error("expected ok with valid auth")
}
@@ -117,7 +117,7 @@ func TestAuthenticate(t *testing.T) {
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
_, ok, _ := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok with failed auth")
}
@@ -138,7 +138,7 @@ func TestAuthenticate_ProbeResistanceCode(t *testing.T) {
w := httptest.NewRecorder()
resp := &http.Response{Header: http.Header{}, Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
_, ok, _ := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
@@ -155,7 +155,7 @@ func TestAuthenticate_ProbeResistanceKnockBypass(t *testing.T) {
w := httptest.NewRecorder()
resp := &http.Response{Header: http.Header{}, Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
_, ok, _ := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
@@ -164,6 +164,43 @@ func TestAuthenticate_ProbeResistanceKnockBypass(t *testing.T) {
}
}
func TestAuthenticate_ProbeResistanceHost(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
h.md.probeResistance = &probeResistance{Type: "host", Value: "127.0.0.1:9999"}
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: http.Header{}, Body: io.NopCloser(strings.NewReader(""))}
_, ok, pipeTo := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
if pipeTo != "127.0.0.1:9999" {
t.Errorf("pipeTo = %q, want %q", pipeTo, "127.0.0.1:9999")
}
}
func TestAuthenticate_ProbeResistanceHostKnockMatch(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
h.md.probeResistance = &probeResistance{Type: "host", Value: "127.0.0.1:9999", Knock: "example.com"}
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: http.Header{}, Body: io.NopCloser(strings.NewReader(""))}
_, ok, pipeTo := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
if pipeTo != "" {
t.Errorf("pipeTo = %q, want empty (knock matched, probe resistance bypassed)", pipeTo)
}
if resp.StatusCode != http.StatusProxyAuthRequired {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusProxyAuthRequired)
}
}
func TestAuthenticate_CustomRealm(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
h.md.authBasicRealm = "testrealm"
@@ -172,7 +209,7 @@ func TestAuthenticate_CustomRealm(t *testing.T) {
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
_, ok, _ := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}