Files
Leonov Vladislav 1c42c638a6 recorder: emit periodic session snapshots and byte deltas
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.
2026-09-15 15:05:02 +03:00

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
}