722dde5cfc
The readTimeout field (default 15s) was being applied via xnet.Pipe to both directions of a bidirectional proxy connection. During asymmetric transfers (e.g. HTTP file download), the direction reading from the tunnel sees no data after the initial request is forwarded, causing SetReadDeadline to fire after 15s and abort the entire transfer. Fix: add a separate idleTimeout field (default 0=disabled) to the metadata structs in both forward/local and forward/remote handlers, and switch xnet.Pipe to use idleTimeout instead of readTimeout. The readTimeout field now only applies to the initial protocol sniffing/handshake phase. Also document readTimeout vs idleTimeout semantics across all 24 locations in the x/ module where these timeouts appear: - readTimeout: handshake sniffing deadline (handlers), upstream response header timeout (http.Transport), or transport-level read deadline - idleTimeout: idle read deadline per Pipe direction (0=disabled) - ReadTimeout on Sniffer/SnifferBuilder: upstream response header/TLS handshake read timeout during sniffing
153 lines
5.9 KiB
Go
153 lines
5.9 KiB
Go
package http
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/bypass"
|
|
mdata "github.com/go-gost/core/metadata"
|
|
mdutil "github.com/go-gost/x/metadata/util"
|
|
"github.com/go-gost/x/registry"
|
|
)
|
|
|
|
const (
|
|
defaultRealm = "gost"
|
|
defaultProxyAgent = "gost/3.0"
|
|
)
|
|
|
|
// metadata holds all configuration parsed from the handler's metadata map.
|
|
// Fields are populated by parseMetadata and read throughout the handler's
|
|
// request-processing methods.
|
|
type metadata struct {
|
|
readTimeout time.Duration // deadline for upstream response headers (http.Transport.ResponseHeaderTimeout); 0=15s default, negative=disabled
|
|
idleTimeout time.Duration // idle read deadline per Pipe direction during CONNECT/forwarding; 0 or negative = disabled
|
|
keepalive bool // enable HTTP keep-alive on the upstream transport
|
|
compression bool // enable HTTP compression on the upstream transport
|
|
|
|
probeResistance *probeResistance // decoy response config for auth failures (nil = disabled)
|
|
enableUDP bool // allow UDP relay over HTTP
|
|
udpBufferSize int // UDP relay buffer size in bytes
|
|
|
|
header http.Header // additional response headers added to every proxy response
|
|
hash string // hash strategy for hop selection ("host" or empty)
|
|
authBasicRealm string // custom realm for Basic auth 407 responses
|
|
proxyAgent string // Proxy-Agent header value; default "gost/3.0"
|
|
|
|
observerPeriod time.Duration // stats reporting interval; default 5s, min 1s
|
|
observerResetTraffic bool // reset traffic counters after each observation
|
|
|
|
sniffing bool // enable protocol sniffing on CONNECT tunnels
|
|
sniffingTimeout time.Duration // deadline for the initial sniff read
|
|
sniffingWebsocket bool // enable WebSocket frame recording
|
|
sniffingWebsocketSampleRate float64 // max frames recorded per second
|
|
|
|
certificate *x509.Certificate // MITM CA certificate (for TLS decryption)
|
|
privateKey crypto.PrivateKey // MITM CA private key
|
|
alpn string // ALPN protocol to negotiate during MITM
|
|
mitmBypass bypass.Bypass // bypass rules that skip MITM decryption
|
|
|
|
limiterRefreshInterval time.Duration // traffic limiter cache refresh interval
|
|
limiterCleanupInterval time.Duration // traffic limiter cache cleanup interval
|
|
}
|
|
|
|
// parseMetadata reads all configuration values from the metadata map into
|
|
// the handler's metadata struct. It applies defaults for readTimeout (15s),
|
|
// observerPeriod (5s, min 1s), and proxyAgent ("gost/3.0"). MITM TLS is
|
|
// enabled when both mitm.certFile and mitm.keyFile are provided.
|
|
func (h *httpHandler) parseMetadata(md mdata.Metadata) error {
|
|
h.md.readTimeout = mdutil.GetDuration(md, "readTimeout")
|
|
if h.md.readTimeout == 0 {
|
|
h.md.readTimeout = 15 * time.Second
|
|
}
|
|
if h.md.readTimeout < 0 {
|
|
h.md.readTimeout = 0
|
|
}
|
|
|
|
h.md.idleTimeout = mdutil.GetDuration(md, "idleTimeout")
|
|
if h.md.idleTimeout < 0 {
|
|
h.md.idleTimeout = 0
|
|
}
|
|
|
|
if m := mdutil.GetStringMapString(md, "http.header", "header"); len(m) > 0 {
|
|
hd := http.Header{}
|
|
for k, v := range m {
|
|
hd.Add(k, v)
|
|
}
|
|
h.md.header = hd
|
|
}
|
|
|
|
h.md.keepalive = mdutil.GetBool(md, "http.keepalive", "keepalive")
|
|
h.md.compression = mdutil.GetBool(md, "http.compression", "compression")
|
|
|
|
// probeResist format: "type:value" (e.g. "code:404", "web:example.com",
|
|
// "host:192.168.1.1:80", "file:/var/www/index.html").
|
|
if pr := mdutil.GetString(md, "probeResist", "probe_resist"); pr != "" {
|
|
if ss := strings.SplitN(pr, ":", 2); len(ss) == 2 {
|
|
h.md.probeResistance = &probeResistance{
|
|
Type: ss[0],
|
|
Value: ss[1],
|
|
Knock: mdutil.GetString(md, "knock"),
|
|
}
|
|
}
|
|
}
|
|
h.md.enableUDP = mdutil.GetBool(md, "udp")
|
|
h.md.udpBufferSize = mdutil.GetInt(md, "udp.bufferSize", "udpBufferSize")
|
|
h.md.hash = mdutil.GetString(md, "hash")
|
|
h.md.authBasicRealm = mdutil.GetString(md, "authBasicRealm")
|
|
|
|
h.md.observerPeriod = mdutil.GetDuration(md, "observePeriod", "observer.period", "observer.observePeriod")
|
|
if h.md.observerPeriod == 0 {
|
|
h.md.observerPeriod = 5 * time.Second
|
|
}
|
|
if h.md.observerPeriod < time.Second {
|
|
h.md.observerPeriod = time.Second
|
|
}
|
|
|
|
h.md.observerResetTraffic = mdutil.GetBool(md, "observer.resetTraffic")
|
|
|
|
h.md.proxyAgent = mdutil.GetString(md, "http.proxyAgent", "proxyAgent")
|
|
if h.md.proxyAgent == "" {
|
|
h.md.proxyAgent = defaultProxyAgent
|
|
}
|
|
|
|
h.md.sniffing = mdutil.GetBool(md, "sniffing")
|
|
h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout")
|
|
h.md.sniffingWebsocket = mdutil.GetBool(md, "sniffing.websocket")
|
|
h.md.sniffingWebsocketSampleRate = mdutil.GetFloat(md, "sniffing.websocket.sampleRate")
|
|
|
|
certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile")
|
|
keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile")
|
|
if certFile != "" && keyFile != "" {
|
|
tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.md.privateKey = tlsCert.PrivateKey
|
|
}
|
|
h.md.alpn = mdutil.GetString(md, "mitm.alpn")
|
|
h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass"))
|
|
|
|
h.md.limiterRefreshInterval = mdutil.GetDuration(md, "limiter.refreshInterval")
|
|
h.md.limiterCleanupInterval = mdutil.GetDuration(md, "limiter.cleanupInterval")
|
|
|
|
return nil
|
|
}
|
|
|
|
// probeResistance configures a decoy response that hides the proxy from
|
|
// unauthorised clients. When authentication fails, instead of returning
|
|
// 407 Proxy-Auth-Required, the handler returns a fake response that makes
|
|
// the port appear to run a different service.
|
|
type probeResistance struct {
|
|
Type string // strategy: "code", "web", "host", or "file"
|
|
Value string // strategy-specific parameter (status code, URL, address, path)
|
|
Knock string // optional comma-separated hostnames; probe resistance only fires when the request hostname matches none of them
|
|
}
|