Files
x/handler/http/metadata.go
T
ginuerzh c246a1d4fa feat(handler/http,http2): support comma-separated hostnames in probeResistance knock
Knock previously accepted a single hostname. Now it accepts multiple
comma-separated hostnames; probe resistance is bypassed if the request
hostname matches any entry in the list. Matching is case-insensitive
and whitespace around entries is trimmed.
2026-05-28 23:20:27 +08:00

153 lines
5.8 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 // read timeout for upstream responses; default 15s, negative disables
idleTimeout time.Duration // idle timeout for pipe forwarding; 0 means 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
}