fix(handler/http): host validation with port split, sniffer ownership guard, body leak, probe resistance hardening
- Split host:port in Host header before DNS/IP validation, accept bare IPs - Guard sniffer connection ownership with snifferHandled flag to prevent double-close when sniffers take over the tunnel - Close req.Body in handleProxy keep-alive loop on both error and success paths to prevent goroutine leaks from idle HTTP transport connections - Log all resp.Write errors (5 call sites) instead of silently discarding - Respect resp.Close for all responses, not just ContentLength >= 0, fixing unnecessarily closed chunked keep-alive connections - Harden probe resistance: validate strconv.Atoi result, add 15s timeout to web redirect HTTP client, document pr.Knock empty/set/mismatch logic
This commit is contained in:
+48
-14
@@ -198,8 +198,14 @@ func (h *httpHandler) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||||
if !req.URL.IsAbs() && govalidator.IsDNSName(req.Host) {
|
if !req.URL.IsAbs() {
|
||||||
req.URL.Scheme = "http"
|
host := req.Host
|
||||||
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
if govalidator.IsDNSName(host) || net.ParseIP(host) != nil {
|
||||||
|
req.URL.Scheme = "http"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
network := req.Header.Get("X-Gost-Protocol")
|
network := req.Header.Get("X-Gost-Protocol")
|
||||||
@@ -306,7 +312,9 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
|
|||||||
log.Trace(string(dump))
|
log.Trace(string(dump))
|
||||||
}
|
}
|
||||||
log.Debug("bypass: ", addr)
|
log.Debug("bypass: ", addr)
|
||||||
resp.Write(conn)
|
if err := resp.Write(conn); err != nil {
|
||||||
|
log.Error("write bypass response: ", err)
|
||||||
|
}
|
||||||
return xbypass.ErrBypass
|
return xbypass.ErrBypass
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,10 +360,17 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
|
|||||||
dump, _ := httputil.DumpResponse(resp, false)
|
dump, _ := httputil.DumpResponse(resp, false)
|
||||||
log.Trace(string(dump))
|
log.Trace(string(dump))
|
||||||
}
|
}
|
||||||
resp.Write(conn)
|
if err := resp.Write(conn); err != nil {
|
||||||
|
log.Error("write error response: ", err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cc.Close()
|
snifferHandled := false
|
||||||
|
defer func() {
|
||||||
|
if !snifferHandled {
|
||||||
|
cc.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
||||||
ro.SrcAddr = cc.LocalAddr().String()
|
ro.SrcAddr = cc.LocalAddr().String()
|
||||||
@@ -407,6 +422,7 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
|
|||||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||||
switch proto {
|
switch proto {
|
||||||
case sniffing.ProtoHTTP:
|
case sniffing.ProtoHTTP:
|
||||||
|
snifferHandled = true
|
||||||
return sniffer.HandleHTTP(ctx, "tcp", conn,
|
return sniffer.HandleHTTP(ctx, "tcp", conn,
|
||||||
sniffing.WithService(h.options.Service),
|
sniffing.WithService(h.options.Service),
|
||||||
sniffing.WithDial(dial),
|
sniffing.WithDial(dial),
|
||||||
@@ -415,6 +431,7 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt
|
|||||||
sniffing.WithLog(log),
|
sniffing.WithLog(log),
|
||||||
)
|
)
|
||||||
case sniffing.ProtoTLS:
|
case sniffing.ProtoTLS:
|
||||||
|
snifferHandled = true
|
||||||
return sniffer.HandleTLS(ctx, "tcp", conn,
|
return sniffer.HandleTLS(ctx, "tcp", conn,
|
||||||
sniffing.WithService(h.options.Service),
|
sniffing.WithService(h.options.Service),
|
||||||
sniffing.WithDial(dial),
|
sniffing.WithDial(dial),
|
||||||
@@ -464,8 +481,10 @@ func (h *httpHandler) handleProxy(ctx context.Context, conn net.Conn, req *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if close, err := h.proxyRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), req, ro, &pStats, log); err != nil || close {
|
if close, err := h.proxyRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), req, ro, &pStats, log); err != nil || close {
|
||||||
|
req.Body.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
req.Body.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +569,9 @@ func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriteCloser,
|
|||||||
log.Trace(string(dump))
|
log.Trace(string(dump))
|
||||||
}
|
}
|
||||||
log.Debug("bypass: ", host)
|
log.Debug("bypass: ", host)
|
||||||
res.Write(rw)
|
if werr := res.Write(rw); werr != nil {
|
||||||
|
log.Error("write bypass response: ", werr)
|
||||||
|
}
|
||||||
err = xbypass.ErrBypass
|
err = xbypass.ErrBypass
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -581,7 +602,9 @@ func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriteCloser,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
res.Write(rw)
|
if werr := res.Write(rw); werr != nil {
|
||||||
|
log.Error("write error response: ", werr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -634,9 +657,7 @@ func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriteCloser,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.ContentLength >= 0 {
|
close = resp.Close
|
||||||
close = resp.Close
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -864,19 +885,30 @@ func (h *httpHandler) authenticate(ctx context.Context, conn net.Conn, req *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
pr := h.md.probeResistance
|
pr := h.md.probeResistance
|
||||||
// probing resistance is enabled, and knocking host is mismatch.
|
// Probe resistance activates on auth failure in two cases:
|
||||||
|
// 1. pr.Knock is empty — no specific knock host; always hide the proxy.
|
||||||
|
// 2. pr.Knock is set but the request hostname does not match — the client
|
||||||
|
// didn't "knock" on the right host, so a fake response is returned.
|
||||||
|
// When the request hostname matches a non-empty pr.Knock, probe resistance
|
||||||
|
// is bypassed and falls through to the normal 407 Proxy-Auth-Required below,
|
||||||
|
// revealing the proxy only to clients that know the knock address.
|
||||||
if pr != nil && (pr.Knock == "" || !strings.EqualFold(req.URL.Hostname(), pr.Knock)) {
|
if pr != nil && (pr.Knock == "" || !strings.EqualFold(req.URL.Hostname(), pr.Knock)) {
|
||||||
resp.StatusCode = http.StatusServiceUnavailable // default status code
|
resp.StatusCode = http.StatusServiceUnavailable // default status code
|
||||||
|
|
||||||
switch pr.Type {
|
switch pr.Type {
|
||||||
case "code":
|
case "code":
|
||||||
resp.StatusCode, _ = strconv.Atoi(pr.Value)
|
if code, err := strconv.Atoi(pr.Value); err == nil {
|
||||||
|
resp.StatusCode = code
|
||||||
|
} else {
|
||||||
|
log.Warnf("invalid probe resistance code: %s", pr.Value)
|
||||||
|
}
|
||||||
case "web":
|
case "web":
|
||||||
url := pr.Value
|
url := pr.Value
|
||||||
if !strings.HasPrefix(url, "http") {
|
if !strings.HasPrefix(url, "http") {
|
||||||
url = "http://" + url
|
url = "http://" + url
|
||||||
}
|
}
|
||||||
r, err := http.Get(url)
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
r, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
break
|
break
|
||||||
@@ -941,7 +973,9 @@ func (h *httpHandler) authenticate(ctx context.Context, conn net.Conn, req *http
|
|||||||
log.Trace(string(dump))
|
log.Trace(string(dump))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Write(conn)
|
if err := resp.Write(conn); err != nil {
|
||||||
|
log.Error("write auth response: ", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user