mirror of
https://github.com/go-gost/x.git
synced 2026-10-20 08:18:33 +00:00
Handlers write one recorder entry when a session ends, so long-lived SOCKS5 tunnels, streaming HTTP responses and UDP associations stay invisible while they run and report their traffic as one late record. With recorder.period set, a session now emits interim records on the interval while it is active and one final record when it ends. Each record carries the cumulative counters alongside the increments since the previous one, so a consumer sums records instead of replacing them, plus sessionID, recordIndex and phase. A session that ends inside one period still writes a single record, as it does today. One goroutine per handler samples every live session on the tick and writes its record straight to the recorder. There is no queue and no retry loop, and none is needed: a session moves its cursor past a record only once the recorder has taken it, so a refused write costs no bytes. They stay in the delta and go out with the next record. Holding on to what the sink cannot take right now is the sink's job. Interim and Finish take a context and return an error, so a handler learns what became of every record it asked for. Start writes nothing, so it takes neither. Close stops the sampling and reports the sessions its owner had not finished; nothing is buffered, so there is nothing to drain. With recorder.period unset or non-positive the previous behaviour is kept: one record per session, written inline, in the old shape. An object its handler never stamped, and one whose record mode is off, is dropped in both modes.
157 lines
6.1 KiB
Go
157 lines
6.1 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 {
|
|
recorderPeriod time.Duration // reports a live session on this interval; 0 = one record per session, written when it ends.
|
|
|
|
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.recorderPeriod = mdutil.GetDuration(md, "recorder.period", "recorder.reportPeriod")
|
|
|
|
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
|
|
}
|