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
+45 -8
View File
@@ -81,14 +81,10 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req
}
log.Debugf("%s >> %s", req.RemoteAddr, host)
for k := range h.md.header {
w.Header().Set(k, h.md.header.Get(k))
}
resp := &http.Response{
ProtoMajor: 2,
ProtoMinor: 0,
Header: w.Header(),
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader([]byte{})),
}
@@ -108,8 +104,20 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req
ro.HTTP.Response.Header = resp.Header
}()
clientID, ok := h.authenticate(ctx, w, req, resp, log)
clientID, ok, pipeTo := h.authenticate(ctx, w, req, resp, log)
if !ok {
if pipeTo != "" {
cc, err := net.Dial("tcp", pipeTo)
if err != nil {
log.Error(err)
resp.StatusCode = http.StatusServiceUnavailable
h.writeResponse(w, resp)
return ErrAuthFailed
}
defer cc.Close()
h.forwardRequest(w, req, cc)
return ErrAuthFailed
}
return ErrAuthFailed
}
@@ -131,6 +139,10 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req
req.Header.Del("Gost-Target")
req.Header.Del("X-Gost-Target")
for k := range h.md.header {
w.Header().Set(k, h.md.header.Get(k))
}
switch h.md.hash {
case "host":
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: host})
@@ -152,9 +164,34 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req
ro.DstAddr = cc.RemoteAddr().String()
if req.Method != http.MethodConnect {
rw := traffic_wrapper.WrapReadWriter(
h.limiter,
cc,
clientID,
limiter.ScopeOption(limiter.ScopeClient),
limiter.ServiceOption(h.options.Service),
limiter.NetworkOption("tcp"),
limiter.AddrOption(host),
limiter.ClientOption(clientID),
limiter.SrcOption(req.RemoteAddr),
)
if h.options.Observer != nil {
pstats := h.stats.Stats(clientID)
pstats.Add(stats.KindTotalConns, 1)
pstats.Add(stats.KindCurrentConns, 1)
defer pstats.Add(stats.KindCurrentConns, -1)
rw = stats_wrapper.WrapReadWriter(rw, pstats)
}
start := time.Now()
log.Infof("%s <-> %s", req.RemoteAddr, host)
err = h.forwardRequest(w, req, cc)
err = h.forwardRequest(w, req, rw)
if err != nil {
resp.StatusCode = http.StatusServiceUnavailable
if werr := h.writeResponse(w, resp); werr != nil {
log.Error("write error response: ", werr)
}
}
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >-< %s", req.RemoteAddr, host)
@@ -207,7 +244,7 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req
start := time.Now()
log.Infof("%s <-> %s", req.RemoteAddr, host)
// xnet.Transport(rw, cc)
xnet.Pipe(ctx, xio.NewReadWriteCloser(rw, rw, req.Body), cc)
xnet.Pipe(ctx, xio.NewReadWriteCloser(rw, rw, req.Body), cc, xnet.WithReadTimeout(h.md.idleTimeout))
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >-< %s", req.RemoteAddr, host)