mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99a0f7323d | ||
|
|
4057ac54b8 | ||
|
|
6a0ce8914e | ||
|
|
03f4b1291e | ||
|
|
2e16457467 | ||
|
|
a1d7da4fec | ||
|
|
0522ae8155 | ||
|
|
55a246869e | ||
|
|
3ec3362ac4 | ||
|
|
3f01456480 | ||
|
|
9ea10763e4 | ||
|
|
e814852ca1 | ||
|
|
a102f9c6cf | ||
|
|
9ccf402ed1 | ||
|
|
61eb3e2a3b | ||
|
|
92002fa35e | ||
|
|
a424148018 | ||
|
|
d4d823df45 | ||
|
|
c45d27113e | ||
|
|
9fab332772 |
@@ -466,6 +466,17 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// stdio listener uses url.Host as the forwarding target.
|
||||
if listener == "stdio" && len(nodes) == 0 && url.Host != "" {
|
||||
nodes = append(nodes, &config.ForwardNodeConfig{
|
||||
Name: fmt.Sprintf("%starget-0", namePrefix),
|
||||
Addr: url.Host,
|
||||
})
|
||||
if handler == "auto" {
|
||||
handler = "tcp"
|
||||
}
|
||||
}
|
||||
|
||||
if len(nodes) > 0 {
|
||||
if len(services) == 1 {
|
||||
services[0].Forwarder = &config.ForwarderConfig{
|
||||
|
||||
@@ -34,11 +34,27 @@ import (
|
||||
"github.com/go-gost/x/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
xstats "github.com/go-gost/x/observer/stats"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"github.com/go-gost/x/registry"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/vishvananda/netns"
|
||||
)
|
||||
|
||||
// plaintextListeners are listener types that never terminate TLS on their
|
||||
// accepted connections, so any certFile/keyFile/caFile configured for them is
|
||||
// silently ignored. This catches the common footgun where a user writes e.g.
|
||||
// `tls+mws://...?certFile=...` expecting TLS: the "tls+" prefix is parsed as
|
||||
// the handler scheme (see x/config/cmd/cmd.go buildServiceConfig), not a TLS
|
||||
// wrapper, so the underlying mws listener stays plaintext. Extend this set when
|
||||
// new plaintext listeners are added.
|
||||
var plaintextListeners = map[string]bool{
|
||||
"tcp": true, "udp": true,
|
||||
"ws": true, "mws": true,
|
||||
"redirect": true, "tproxy": true,
|
||||
"rtcp": true, "rudp": true,
|
||||
"unix": true, "runix": true, "serial": true, "stdio": true,
|
||||
}
|
||||
|
||||
// ParseService constructs a fully-wired service.Service from a ServiceConfig.
|
||||
// It defaults the listener to "tcp" and the handler to "auto", resolves named
|
||||
// components from the registry (authers, admissions, bypasses, resolvers,
|
||||
@@ -88,6 +104,11 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
tls_util.RejectUnknownSNIConfig(tlsConfig, tlsCfg.RejectUnknownSNI, tlsCfg.ServerNames)
|
||||
}
|
||||
|
||||
if (tlsCfg.CertFile != "" || tlsCfg.KeyFile != "" || tlsCfg.CAFile != "") && plaintextListeners[cfg.Listener.Type] {
|
||||
serviceLogger.Warnf("TLS certificate options are configured but the %q listener does not use TLS and will ignore them; the connection will be plaintext. Use a TLS-capable listener (e.g. mwss, wss, tls, quic, grpc, http2, http3) to enable TLS.",
|
||||
cfg.Listener.Type)
|
||||
}
|
||||
|
||||
authers := auth_parser.List(cfg.Listener.Auther, cfg.Listener.Authers...)
|
||||
if len(authers) == 0 {
|
||||
if auther := auth_parser.ParseAutherFromAuth(cfg.Listener.Auth); auther != nil {
|
||||
@@ -285,8 +306,15 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
var recorders []recorder.RecorderObject
|
||||
for _, r := range cfg.Recorders {
|
||||
md := metadata.NewMetadata(r.Metadata)
|
||||
rec := registry.RecorderRegistry().Get(r.Name)
|
||||
if r.Metadata != nil {
|
||||
rec = &xrecorder.MetadataRecorder{
|
||||
Recorder: rec,
|
||||
Metadata: r.Metadata,
|
||||
}
|
||||
}
|
||||
recorders = append(recorders, recorder.RecorderObject{
|
||||
Recorder: registry.RecorderRegistry().Get(r.Name),
|
||||
Recorder: rec,
|
||||
Record: r.Record,
|
||||
Options: &recorder.Options{
|
||||
Direction: mdutil.GetBool(md, parsing.MDKeyRecorderDirection),
|
||||
@@ -295,6 +323,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
HTTPBody: mdutil.GetBool(md, parsing.MDKeyRecorderHTTPBody),
|
||||
MaxBodySize: mdutil.GetInt(md, parsing.MDKeyRecorderHTTPMaxBodySize),
|
||||
},
|
||||
Metadata: r.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package quic
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
@@ -12,7 +13,9 @@ type quicSession struct {
|
||||
}
|
||||
|
||||
func (session *quicSession) GetConn() (*quicConn, error) {
|
||||
stream, err := session.session.OpenStreamSync(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
stream, err := session.session.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
}
|
||||
|
||||
d.sessionMutex.Lock()
|
||||
defer d.sessionMutex.Unlock()
|
||||
|
||||
session, ok := d.sessions[addr]
|
||||
if session != nil && session.IsClosed() {
|
||||
@@ -95,6 +94,7 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
pc, err = icmp_pkg.ListenPacket("ip4:icmp", "")
|
||||
}
|
||||
if err != nil {
|
||||
d.sessionMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,16 +109,22 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
if err != nil {
|
||||
d.logger.Error(err)
|
||||
pc.Close()
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.sessions[addr] = session
|
||||
}
|
||||
d.sessionMutex.Unlock()
|
||||
|
||||
conn, err = session.GetConn()
|
||||
if err != nil {
|
||||
d.sessionMutex.Lock()
|
||||
if d.sessions[addr] == session {
|
||||
delete(d.sessions, addr)
|
||||
}
|
||||
session.Close()
|
||||
delete(d.sessions, addr)
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+11
-4
@@ -14,14 +14,21 @@ type metadata struct {
|
||||
}
|
||||
|
||||
func (d *icmpDialer) parseMetadata(md mdata.Metadata) (err error) {
|
||||
if mdutil.GetBool(md, "keepalive") {
|
||||
d.md.keepAlivePeriod = mdutil.GetDuration(md, "ttl")
|
||||
const (
|
||||
keepAlive = "keepAlive"
|
||||
keepAlivePeriod = "ttl"
|
||||
handshakeTimeout = "handshakeTimeout"
|
||||
maxIdleTimeout = "maxIdleTimeout"
|
||||
)
|
||||
|
||||
if md == nil || !md.IsExists(keepAlive) || mdutil.GetBool(md, keepAlive) {
|
||||
d.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod)
|
||||
if d.md.keepAlivePeriod <= 0 {
|
||||
d.md.keepAlivePeriod = 10 * time.Second
|
||||
}
|
||||
}
|
||||
d.md.handshakeTimeout = mdutil.GetDuration(md, "handshakeTimeout")
|
||||
d.md.maxIdleTimeout = mdutil.GetDuration(md, "maxIdleTimeout")
|
||||
d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
|
||||
d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package quic
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
@@ -12,7 +13,9 @@ type quicSession struct {
|
||||
}
|
||||
|
||||
func (session *quicSession) GetConn() (*quicConn, error) {
|
||||
stream, err := session.session.OpenStreamSync(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
stream, err := session.session.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
}
|
||||
|
||||
d.sessionMutex.Lock()
|
||||
defer d.sessionMutex.Unlock()
|
||||
|
||||
session, ok := d.sessions[addr]
|
||||
if session != nil && session.IsClosed() {
|
||||
@@ -73,11 +72,13 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
|
||||
c, err := options.Dialer.Dial(ctx, "udp", "")
|
||||
if err != nil {
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
pc, ok := c.(net.PacketConn)
|
||||
if !ok {
|
||||
c.Close()
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, errors.New("quic: wrong connection type")
|
||||
}
|
||||
|
||||
@@ -89,16 +90,22 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
if err != nil {
|
||||
d.logger.Error(err)
|
||||
pc.Close()
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.sessions[addr] = session
|
||||
}
|
||||
d.sessionMutex.Unlock()
|
||||
|
||||
conn, err = session.GetConn()
|
||||
if err != nil {
|
||||
d.sessionMutex.Lock()
|
||||
if d.sessions[addr] == session {
|
||||
delete(d.sessions, addr)
|
||||
}
|
||||
session.Close()
|
||||
delete(d.sessions, addr)
|
||||
d.sessionMutex.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,18 @@ func (d *sshdDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
||||
delete(d.sessions, addr) // session is dead
|
||||
ok = false
|
||||
}
|
||||
if ok {
|
||||
// Health check: verify the cached session is still alive before
|
||||
// returning it. A dead-but-not-yet-detected session (e.g. idle
|
||||
// connection silently dropped by NAT) would cause "ssh: unexpected
|
||||
// packet in response to channel open" later in the connector.
|
||||
if err := session.Ping(d.md.keepaliveTimeout); err != nil {
|
||||
d.options.Logger.Debugf("sshd session ping failed: %v, recreating", err)
|
||||
session.Close()
|
||||
delete(d.sessions, addr)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
var options dialer.DialOptions
|
||||
for _, opt := range opts {
|
||||
|
||||
@@ -7,12 +7,12 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-gost/core v0.4.1
|
||||
github.com/go-gost/core v0.4.2
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3
|
||||
github.com/go-gost/gosocks4 v0.1.0
|
||||
github.com/go-gost/gosocks5 v0.5.0
|
||||
github.com/go-gost/plugin v0.3.0
|
||||
github.com/go-gost/relay v0.6.0
|
||||
github.com/go-gost/relay v0.6.1
|
||||
github.com/go-gost/tls-dissector v0.2.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/gobwas/glob v0.2.3
|
||||
@@ -60,6 +60,7 @@ require (
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gost/core v0.4.1 h1:/MZYv0tSkw9yoK2nl0GLl3GjT1/3qKl5u3E/DADyeZE=
|
||||
github.com/go-gost/core v0.4.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/core v0.4.2 h1:NBCkdWItIiwt15cA2mkRza/u6ukxQVSW74bc33o70zE=
|
||||
github.com/go-gost/core v0.4.2/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A=
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
|
||||
github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk=
|
||||
@@ -61,8 +61,8 @@ github.com/go-gost/gosocks5 v0.5.0 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yi
|
||||
github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
|
||||
github.com/go-gost/plugin v0.3.0 h1:pmll8nNd9PX92BWMB5+b2y2SEkBAWJLxD2ANfX+WHuw=
|
||||
github.com/go-gost/plugin v0.3.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
|
||||
github.com/go-gost/relay v0.6.0 h1:o8r0B3eTw8Rwwent+5x5Cw1g4AyNUl4hNqiyDJXkFvA=
|
||||
github.com/go-gost/relay v0.6.0/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
||||
github.com/go-gost/relay v0.6.1 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc=
|
||||
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
||||
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
|
||||
github.com/go-gost/tls-dissector v0.2.0/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
@@ -206,6 +206,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
||||
+45
-1
@@ -2,6 +2,7 @@ package auto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
@@ -12,7 +13,10 @@ import (
|
||||
"github.com/go-gost/gosocks4"
|
||||
"github.com/go-gost/gosocks5"
|
||||
ctxvalue "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
"github.com/go-gost/x/internal/util/sniffing"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
@@ -94,6 +98,16 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
proto, _ := sniffing.Sniff(ctx, br)
|
||||
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
|
||||
if proto == sniffing.ProtoTLS {
|
||||
return h.handleTLS(ctx, conn, log)
|
||||
}
|
||||
|
||||
// Fall back to 1-byte peek for SOCKS4 (0x04) vs SOCKS5 (0x05) vs HTTP.
|
||||
// Sniff() already read 5 bytes, but the buffered reader exposes them.
|
||||
b, err := br.Peek(1)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -101,7 +115,6 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
return err
|
||||
}
|
||||
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
switch b[0] {
|
||||
case gosocks4.Ver4: // socks4
|
||||
if h.socks4Handler != nil {
|
||||
@@ -118,3 +131,34 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleTLS forwards a TLS connection that arrived at the auto handler.
|
||||
// It parses the ClientHello for SNI, dials upstream through the router,
|
||||
// forwards the ClientHello, records ServerHello metadata, and pipes data
|
||||
// bidirectionally.
|
||||
func (h *autoHandler) handleTLS(ctx context.Context, conn net.Conn, log logger.Logger) error {
|
||||
ro := &xrecorder.HandlerRecorderObject{
|
||||
Network: "tcp",
|
||||
Service: h.options.Service,
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
LocalAddr: conn.LocalAddr().String(),
|
||||
SID: ctxvalue.SidFromContext(ctx).String(),
|
||||
Time: time.Now(),
|
||||
}
|
||||
sniffer := &sniffing.Sniffer{}
|
||||
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var buf bytes.Buffer
|
||||
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ro.Route = buf.String()
|
||||
return cc, nil
|
||||
}
|
||||
return sniffer.HandleTLS(ctx, "tcp", conn,
|
||||
sniffing.WithDial(dial),
|
||||
sniffing.WithBypass(h.options.Bypass),
|
||||
sniffing.WithRecorderObject(ro),
|
||||
sniffing.WithLog(log),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (h *metricsHandler) Init(md md.Metadata) (err error) {
|
||||
|
||||
xmetrics.Enable(true)
|
||||
|
||||
h.handler = promhttp.Handler()
|
||||
h.handler = promhttp.HandlerFor(xmetrics.Registry(), promhttp.HandlerOpts{})
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(h.md.path, http.HandlerFunc(h.handleFunc))
|
||||
|
||||
@@ -100,6 +100,26 @@ func (h *socks5Handler) Init(md md.Metadata) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// networkAddr derives the network string from the SOCKS5 address type.
|
||||
// For IPv4 addresses it returns "tcp4"/"udp4" to prevent dual-stack
|
||||
// (IPv4-mapped-IPv6) socket creation on Linux, which would cause the
|
||||
// listener to bind on [::]:port instead of 0.0.0.0:port.
|
||||
// For IPv6 addresses it returns "tcp6"/"udp6".
|
||||
// For domain addresses it returns the unmodified network.
|
||||
func networkAddr(network string, addr *gosocks5.Addr) string {
|
||||
if addr == nil {
|
||||
return network
|
||||
}
|
||||
switch addr.Type {
|
||||
case gosocks5.AddrIPv4:
|
||||
return network + "4"
|
||||
case gosocks5.AddrIPv6:
|
||||
return network + "6"
|
||||
default:
|
||||
return network
|
||||
}
|
||||
}
|
||||
|
||||
func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -179,15 +199,15 @@ func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handl
|
||||
case gosocks5.CmdConnect:
|
||||
return h.handleConnect(ctx, conn, "tcp", address, ro, log)
|
||||
case gosocks5.CmdBind:
|
||||
return h.handleBind(ctx, conn, "tcp", address, ro, log)
|
||||
return h.handleBind(ctx, conn, networkAddr("tcp", req.Addr), address, ro, log)
|
||||
case socks.CmdMuxBind:
|
||||
return h.handleMuxBind(ctx, conn, "tcp", address, ro, log)
|
||||
return h.handleMuxBind(ctx, conn, networkAddr("tcp", req.Addr), address, ro, log)
|
||||
case gosocks5.CmdUdp:
|
||||
ro.Network = "udp"
|
||||
return h.handleUDP(ctx, conn, "udp", ro, log)
|
||||
case socks.CmdUDPTun:
|
||||
ro.Network = "udp"
|
||||
return h.handleUDPTun(ctx, conn, "udp", address, ro, log)
|
||||
return h.handleUDPTun(ctx, conn, networkAddr("udp", req.Addr), address, ro, log)
|
||||
default:
|
||||
err = ErrUnknownCmd
|
||||
log.Error(err)
|
||||
|
||||
@@ -26,6 +26,7 @@ type metadata struct {
|
||||
enableBind bool
|
||||
enableUDP bool
|
||||
udpBufferSize int
|
||||
udpResolveDomain bool
|
||||
compatibilityMode bool
|
||||
hash string
|
||||
muxCfg *mux.Config
|
||||
@@ -58,6 +59,7 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
h.md.enableBind = mdutil.GetBool(md, "bind")
|
||||
h.md.enableUDP = mdutil.GetBool(md, "udp")
|
||||
h.md.udpBufferSize = mdutil.GetInt(md, "udp.bufferSize", "udpBufferSize")
|
||||
h.md.udpResolveDomain = mdutil.GetBool(md, "udp.resolveDomain", "udpResolveDomain")
|
||||
|
||||
h.md.compatibilityMode = mdutil.GetBool(md, "comp")
|
||||
h.md.hash = mdutil.GetString(md, "hash")
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/gosocks5"
|
||||
)
|
||||
|
||||
func TestNetworkAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
network string
|
||||
addr *gosocks5.Addr
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil addr returns unmodified",
|
||||
network: "tcp",
|
||||
addr: nil,
|
||||
want: "tcp",
|
||||
},
|
||||
{
|
||||
name: "IPv4 tcp returns tcp4",
|
||||
network: "tcp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv4},
|
||||
want: "tcp4",
|
||||
},
|
||||
{
|
||||
name: "IPv6 tcp returns tcp6",
|
||||
network: "tcp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv6},
|
||||
want: "tcp6",
|
||||
},
|
||||
{
|
||||
name: "IPv4 udp returns udp4",
|
||||
network: "udp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv4},
|
||||
want: "udp4",
|
||||
},
|
||||
{
|
||||
name: "IPv6 udp returns udp6",
|
||||
network: "udp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrIPv6},
|
||||
want: "udp6",
|
||||
},
|
||||
{
|
||||
name: "domain addr returns unmodified",
|
||||
network: "tcp",
|
||||
addr: &gosocks5.Addr{Type: gosocks5.AddrDomain},
|
||||
want: "tcp",
|
||||
},
|
||||
{
|
||||
name: "unknown addr type returns unmodified",
|
||||
network: "udp",
|
||||
addr: &gosocks5.Addr{Type: 99},
|
||||
want: "udp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := networkAddr(tt.network, tt.addr)
|
||||
if got != tt.want {
|
||||
t.Errorf("networkAddr(%q, %v) = %q, want %q", tt.network, tt.addr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+75
-2
@@ -6,11 +6,14 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/hosts"
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/gosocks5"
|
||||
ctxvalue "github.com/go-gost/x/ctx"
|
||||
ictx "github.com/go-gost/x/internal/ctx"
|
||||
@@ -129,12 +132,23 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st
|
||||
}
|
||||
}
|
||||
|
||||
r := udp.NewRelay(socks.UDPConn(
|
||||
pc1 := socks.UDPConn(
|
||||
&filteredPacketConn{
|
||||
PacketConn: cc,
|
||||
filterIP: conn.RemoteAddr().(*net.TCPAddr).IP,
|
||||
},
|
||||
h.md.udpBufferSize), pc).
|
||||
h.md.udpBufferSize)
|
||||
|
||||
if h.md.udpResolveDomain {
|
||||
pc1 = &domainResolvePacketConn{
|
||||
PacketConn: pc1,
|
||||
resolver: h.options.Router.Options().Resolver,
|
||||
hostMapper: h.options.Router.Options().HostMapper,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
r := udp.NewRelay(pc1, pc).
|
||||
WithService(h.options.Service).
|
||||
WithBypass(h.options.Bypass).
|
||||
WithBufferSize(h.md.udpBufferSize).
|
||||
@@ -170,3 +184,62 @@ func (f *filteredPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// domainResolvePacketConn wraps a net.PacketConn and resolves domain
|
||||
// addresses to IPs in WriteTo calls. This guarantees that SOCKS5 UDP
|
||||
// response datagrams never carry ATYP=Domain (0x03), which some clients
|
||||
// (e.g., tun2proxy, Surge) cannot parse.
|
||||
type domainResolvePacketConn struct {
|
||||
net.PacketConn
|
||||
resolver resolver.Resolver
|
||||
hostMapper hosts.HostMapper
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func (c *domainResolvePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
host, portStr, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
}
|
||||
if net.ParseIP(host) != nil {
|
||||
// Already an IP address — pass through unchanged.
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
// Resolve the domain using the router's infrastructure
|
||||
// (host mapper → resolver → system fallback) so the SOCKS5 encoder
|
||||
// will use AddrIPv4 or AddrIPv6 instead of AddrDomain.
|
||||
var ips []net.IP
|
||||
if c.hostMapper != nil {
|
||||
ips, _ = c.hostMapper.Lookup(context.Background(), "ip", host)
|
||||
}
|
||||
if len(ips) == 0 && c.resolver != nil {
|
||||
ips, err = c.resolver.Resolve(context.Background(), "ip", host)
|
||||
if err != nil && c.log != nil {
|
||||
c.log.Warnf("socks5 udp: resolve %s: %v", host, err)
|
||||
}
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
ips, err = net.LookupIP(host)
|
||||
}
|
||||
if err != nil || len(ips) == 0 {
|
||||
// Cannot resolve the domain — drop the datagram rather than
|
||||
// forwarding an ATYP=Domain response the client cannot parse.
|
||||
if c.log != nil {
|
||||
c.log.Warnf("socks5 udp: dropping datagram to %s (DNS resolution failed)", host)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Pick the first IPv4 address from the result set, falling back to
|
||||
// the first entry when no IPv4 is available.
|
||||
ip := ips[0]
|
||||
for _, candidate := range ips {
|
||||
if candidate.To4() != nil {
|
||||
ip = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
return c.PacketConn.WriteTo(b, &net.UDPAddr{IP: ip, Port: port})
|
||||
}
|
||||
|
||||
+21
-2
@@ -13,7 +13,8 @@ import (
|
||||
|
||||
// Resolve resolves addr to a concrete IP address. If hosts is non-nil, it is
|
||||
// consulted first. If r is non-nil and no host mapping matched, the resolver
|
||||
// is used. If neither is available, addr is returned unchanged.
|
||||
// is used. If neither is available, Go's native pure-Go resolver is used as a
|
||||
// fallback to avoid cgo-based DNS lookups that would block OS threads.
|
||||
func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) {
|
||||
if addr == "" {
|
||||
return addr, nil
|
||||
@@ -51,5 +52,23 @@ func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hos
|
||||
}
|
||||
return net.JoinHostPort(ips[0].String(), port), nil
|
||||
}
|
||||
return addr, nil
|
||||
|
||||
// A literal IP address needs no resolution.
|
||||
if net.ParseIP(host) != nil {
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Fallback to Go's native (pure-Go) resolver when no custom resolver or
|
||||
// host mapper is configured. This avoids cgo-based DNS lookups in
|
||||
// downstream net.Dialer.DialContext which would block OS threads and
|
||||
// cause thread exhaustion under high connection concurrency.
|
||||
lr := &net.Resolver{PreferGo: true}
|
||||
ips, err := lr.LookupIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return "", fmt.Errorf("resolver: domain %s does not exist", host)
|
||||
}
|
||||
return net.JoinHostPort(ips[0].String(), port), nil
|
||||
}
|
||||
|
||||
@@ -115,13 +115,18 @@ func TestResolve_ResolverEmptyResult(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolve_NoResolverNoHosts(t *testing.T) {
|
||||
// When no resolver and no host mapper are configured, the function now
|
||||
// resolves via Go's native DNS resolver (PreferGo: true). This test
|
||||
// uses an IP address which is short-circuited at the top of Resolve,
|
||||
// avoiding the need for actual DNS resolution in unit tests.
|
||||
// DNS fallback behavior is verified by the e2e tests.
|
||||
ctx := context.Background()
|
||||
addr, err := Resolve(ctx, "tcp", "example.com:8080", nil, nil, nil)
|
||||
addr, err := Resolve(ctx, "tcp", "1.2.3.4:8080", nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if addr != "example.com:8080" {
|
||||
t.Errorf("expected example.com:8080, got %s", addr)
|
||||
if addr != "1.2.3.4:8080" {
|
||||
t.Errorf("expected 1.2.3.4:8080, got %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,9 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleO
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
defer func() { cc.Close() }()
|
||||
|
||||
upstreamHost := normalizeHost(ro.HTTP.Host, "80")
|
||||
|
||||
ho.log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
||||
log = ho.log
|
||||
@@ -112,6 +114,41 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleO
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
// When DNS override directs multiple domains to the same proxy IP,
|
||||
// the browser may reuse a keep-alive connection for a different host.
|
||||
// Re-dial to ensure requests reach the correct upstream.
|
||||
if reqHost := normalizeHost(req.Host, "80"); reqHost != "" && reqHost != upstreamHost {
|
||||
cc.Close()
|
||||
newNode, res, resolveErr := resolveHTTPNode(ctx, reqHost, req, &ho)
|
||||
if resolveErr != nil {
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
return resolveErr
|
||||
}
|
||||
dial := ho.dial
|
||||
if dial == nil {
|
||||
dial = (&net.Dialer{}).DialContext
|
||||
}
|
||||
newCC, dialErr := dial(ctx, "tcp", newNode.Addr)
|
||||
if dialErr != nil {
|
||||
return dialErr
|
||||
}
|
||||
newCC = tlsWrapConn(newCC, newNode.Options().TLS)
|
||||
upstreamHost = reqHost
|
||||
node = newNode
|
||||
cc = newCC
|
||||
ro.Host = reqHost
|
||||
ro.SrcAddr = cc.LocalAddr().String()
|
||||
ro.DstAddr = cc.RemoteAddr().String()
|
||||
log = log.WithFields(map[string]any{
|
||||
"host": reqHost,
|
||||
"node": node.Name,
|
||||
"dst": node.Addr,
|
||||
"src": cc.LocalAddr().String(),
|
||||
})
|
||||
ho.log = log
|
||||
}
|
||||
|
||||
if shouldClose, err := h.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), cc, node, req, &pStats, &ho); err != nil || shouldClose {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (h *Sniffer) HandleTLS(ctx context.Context, conn net.Conn, opts ...HandleOp
|
||||
if ho.log != nil {
|
||||
ho.log.Debugf("no sni in clienthello from %s", conn.RemoteAddr())
|
||||
}
|
||||
return nil
|
||||
return errors.New("tls: sni is empty, closing connection")
|
||||
}
|
||||
ro.Host = host
|
||||
|
||||
|
||||
@@ -93,7 +93,9 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, network string, conn net.Conn,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Close()
|
||||
defer func() { cc.Close() }()
|
||||
|
||||
upstreamHost := host
|
||||
|
||||
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
||||
|
||||
@@ -122,6 +124,26 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, network string, conn net.Conn,
|
||||
log.Trace(string(dump))
|
||||
}
|
||||
|
||||
// When DNS override directs multiple domains to the same proxy IP,
|
||||
// the browser may reuse a keep-alive connection for a different host.
|
||||
// Re-dial to ensure requests reach the correct upstream.
|
||||
if reqHost := normalizeHost(req.Host, "80"); reqHost != upstreamHost {
|
||||
cc.Close()
|
||||
cc, err = dial(ctx, network, reqHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
upstreamHost = reqHost
|
||||
ro.Host = reqHost
|
||||
ro.SrcAddr = cc.LocalAddr().String()
|
||||
ro.DstAddr = cc.RemoteAddr().String()
|
||||
log = log.WithFields(map[string]any{
|
||||
"host": reqHost,
|
||||
"src": cc.LocalAddr().String(),
|
||||
"dst": cc.RemoteAddr().String(),
|
||||
})
|
||||
}
|
||||
|
||||
if shouldClose, err := h.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), cc, req, readTimeout, ro, &pStats, log); err != nil || shouldClose {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
@@ -52,7 +53,7 @@ func (h *Sniffer) HandleTLS(ctx context.Context, network string, conn net.Conn,
|
||||
if log != nil {
|
||||
log.Debugf("no sni in clienthello from %s", conn.RemoteAddr())
|
||||
}
|
||||
return nil
|
||||
return errors.New("tls: sni is empty, closing connection")
|
||||
}
|
||||
ro.Host = host
|
||||
|
||||
|
||||
@@ -114,6 +114,25 @@ func (s *Session) Keepalive(interval, timeout time.Duration, retries int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Ping checks if the SSH session is alive by sending a keepalive request.
|
||||
// It blocks until a response is received or the timeout expires.
|
||||
// A nil error indicates the session is healthy.
|
||||
func (s *Session) Ping(timeout time.Duration) error {
|
||||
if timeout <= 0 {
|
||||
timeout = defaultKeepaliveTimeout
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case err := <-s.ping():
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) ping() <-chan error {
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
|
||||
@@ -59,6 +59,9 @@ func (l *mtcpListener) Init(md md.Metadata) (err error) {
|
||||
lc.SetMultipathTCP(true)
|
||||
l.logger.Debugf("mptcp enabled: %v", lc.MultipathTCP())
|
||||
}
|
||||
if l.md.reuseport {
|
||||
lc.Control = l.setReusePort
|
||||
}
|
||||
ln, err := lc.Listen(context.Background(), network, l.options.Addr)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package mtcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *mtcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package mtcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (l *mtcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
@@ -13,8 +13,9 @@ const (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
mptcp bool
|
||||
muxCfg *mux.Config
|
||||
mptcp bool
|
||||
reuseport bool
|
||||
muxCfg *mux.Config
|
||||
backlog int
|
||||
keepalive bool
|
||||
keepaliveIdle time.Duration
|
||||
@@ -24,6 +25,7 @@ type metadata struct {
|
||||
|
||||
func (l *mtcpListener) parseMetadata(md md.Metadata) (err error) {
|
||||
l.md.mptcp = mdutil.GetBool(md, "mptcp")
|
||||
l.md.reuseport = mdutil.GetBool(md, "reuseport")
|
||||
|
||||
l.md.muxCfg = &mux.Config{
|
||||
Version: mdutil.GetInt(md, "mux.version"),
|
||||
|
||||
@@ -53,7 +53,7 @@ func (l *redirectListener) Init(md md.Metadata) (err error) {
|
||||
network = "tcp4"
|
||||
}
|
||||
lc := net.ListenConfig{}
|
||||
if l.md.tproxy {
|
||||
if l.md.tproxy || l.md.reuseport {
|
||||
lc.Control = l.control
|
||||
}
|
||||
if l.md.mptcp {
|
||||
|
||||
@@ -2,8 +2,17 @@ package tcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
if l.md.reuseport {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,11 +8,18 @@ import (
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("SetsockoptInt(SOL_IP, IP_TRANSPARENT, 1): %v", err)
|
||||
if l.md.tproxy {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set IP_TRANSPARENT: %v", err)
|
||||
}
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IPV6, unix.IPV6_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set IPV6_TRANSPARENT: %v", err)
|
||||
}
|
||||
}
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_IPV6, unix.IPV6_TRANSPARENT, 1); err != nil {
|
||||
l.logger.Errorf("SetsockoptInt(SOL_IPV6, IPV6_TRANSPARENT, 1): %v", err)
|
||||
if l.md.reuseport {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
//go:build !linux && !darwin
|
||||
//go:build !linux && !darwin && !windows
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
return errors.New("TProxy is not available on non-linux platform")
|
||||
if l.md.tproxy {
|
||||
return errors.New("TProxy is not available on non-linux platform")
|
||||
}
|
||||
if l.md.reuseport {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (l *redirectListener) control(network, address string, c syscall.RawConn) error {
|
||||
if l.md.tproxy {
|
||||
return errors.New("TProxy is not available on non-linux platform")
|
||||
}
|
||||
// SO_REUSEADDR is the default on Windows, so reuseport is a no-op.
|
||||
return nil
|
||||
}
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
tproxy bool
|
||||
mptcp bool
|
||||
tproxy bool
|
||||
mptcp bool
|
||||
reuseport bool
|
||||
}
|
||||
|
||||
func (l *redirectListener) parseMetadata(md mdata.Metadata) (err error) {
|
||||
l.md.tproxy = mdutil.GetBool(md, "tproxy")
|
||||
l.md.mptcp = mdutil.GetBool(md, "mptcp")
|
||||
l.md.reuseport = mdutil.GetBool(md, "reuseport")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// Package stdio implements a listener that wraps os.Stdin/os.Stdout as a
|
||||
// net.Listener. It accepts exactly one connection, making it suitable for
|
||||
// SSH ProxyCommand where the parent process (SSH) pipes the transport byte
|
||||
// stream through the child process's standard I/O.
|
||||
//
|
||||
// Usage: gost -L stdio://example.com:22 -F http://proxy:8080
|
||||
package stdio
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.ListenerRegistry().Register("stdio", NewListener)
|
||||
}
|
||||
|
||||
type (
|
||||
stdioListener struct {
|
||||
conn net.Conn
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
accepted bool
|
||||
mu sync.Mutex
|
||||
laddr net.Addr
|
||||
logger logger.Logger
|
||||
md metadata
|
||||
options listener.Options
|
||||
}
|
||||
)
|
||||
|
||||
// NewListener creates a stdio listener. The returned listener wraps
|
||||
// os.Stdin and os.Stdout as a single-use net.Conn.
|
||||
func NewListener(opts ...listener.Option) listener.Listener {
|
||||
options := listener.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
return &stdioListener{
|
||||
logger: options.Logger,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *stdioListener) Init(md md.Metadata) (err error) {
|
||||
if err = l.parseMetadata(md); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.laddr = &stdioAddr{addr: "stdio"}
|
||||
l.done = make(chan struct{})
|
||||
l.conn = &stdioConn{l: l}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Accept returns the stdio connection on the first call.
|
||||
// Subsequent calls block until the listener is closed.
|
||||
func (l *stdioListener) Accept() (net.Conn, error) {
|
||||
l.mu.Lock()
|
||||
if !l.accepted {
|
||||
l.accepted = true
|
||||
l.mu.Unlock()
|
||||
return l.conn, nil
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
<-l.done
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
func (l *stdioListener) Addr() net.Addr {
|
||||
return l.laddr
|
||||
}
|
||||
|
||||
func (l *stdioListener) Close() error {
|
||||
l.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// close signals listener completion. Idempotent and safe for concurrent use.
|
||||
func (l *stdioListener) close() {
|
||||
l.once.Do(func() { close(l.done) })
|
||||
}
|
||||
|
||||
// stdioConn implements net.Conn over os.Stdin and os.Stdout.
|
||||
type stdioConn struct {
|
||||
l *stdioListener
|
||||
}
|
||||
|
||||
func (c *stdioConn) Read(b []byte) (int, error) { return os.Stdin.Read(b) }
|
||||
func (c *stdioConn) Write(b []byte) (int, error) { return os.Stdout.Write(b) }
|
||||
|
||||
// Close terminates the process.
|
||||
//
|
||||
// A stdio listener serves exactly one connection: the process's own
|
||||
// stdin/stdout, piped from a parent such as SSH's ProxyCommand. When that
|
||||
// connection ends the parent has closed the pipe and there is nothing left
|
||||
// to serve, so the whole process must exit.
|
||||
//
|
||||
// Closing the listener alone is not enough: the service's accept loop runs
|
||||
// in a fire-and-forget goroutine (see x/service.Serve) and the process
|
||||
// blocks on the go-svc signal wait (see gost/cmd/gost/main.go), so it would
|
||||
// linger forever after the parent quits. os.Exit is the only reliable way
|
||||
// to terminate here. The handler's deferred stats/recording/logging already
|
||||
// ran before this Close (LIFO order), so nothing is lost.
|
||||
func (c *stdioConn) Close() error {
|
||||
c.l.close()
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stdioConn) LocalAddr() net.Addr { return &stdioAddr{addr: "stdio"} }
|
||||
func (c *stdioConn) RemoteAddr() net.Addr { return &stdioAddr{addr: "pipe"} }
|
||||
|
||||
// Deadline methods are no-ops: os.Stdin/os.Stdout do not support
|
||||
// deadline-based I/O.
|
||||
func (c *stdioConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c *stdioConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c *stdioConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
// stdioAddr is a trivial net.Addr for stdio connections.
|
||||
type stdioAddr struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
func (a *stdioAddr) Network() string { return "tcp" }
|
||||
func (a *stdioAddr) String() string { return a.addr }
|
||||
@@ -0,0 +1,13 @@
|
||||
package stdio
|
||||
|
||||
import (
|
||||
md "github.com/go-gost/core/metadata"
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
// reserved for future options (e.g., timeouts)
|
||||
}
|
||||
|
||||
func (l *stdioListener) parseMetadata(md md.Metadata) (err error) {
|
||||
return
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -68,7 +69,13 @@ func (l *tapListener) Init(md mdata.Metadata) (err error) {
|
||||
func (l *tapListener) listenLoop() {
|
||||
for {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := func() error {
|
||||
err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in createTap: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ifce, name, ip, err := l.createTap()
|
||||
if err != nil {
|
||||
if ifce != nil {
|
||||
|
||||
@@ -56,6 +56,9 @@ func (l *tcpListener) Init(md md.Metadata) (err error) {
|
||||
lc.SetMultipathTCP(true)
|
||||
l.logger.Debugf("mptcp enabled: %v", lc.MultipathTCP())
|
||||
}
|
||||
if l.md.reuseport {
|
||||
lc.Control = l.setReusePort
|
||||
}
|
||||
ln, err := lc.Listen(context.Background(), network, l.options.Addr)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (l *tcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
l.logger.Errorf("failed to set SO_REUSEPORT: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (l *tcpListener) setReusePort(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
mptcp bool
|
||||
mptcp bool
|
||||
reuseport bool
|
||||
|
||||
keepalive bool
|
||||
keepaliveIdle time.Duration
|
||||
@@ -18,6 +19,7 @@ type metadata struct {
|
||||
|
||||
func (l *tcpListener) parseMetadata(md md.Metadata) (err error) {
|
||||
l.md.mptcp = mdutil.GetBool(md, "mptcp")
|
||||
l.md.reuseport = mdutil.GetBool(md, "reuseport")
|
||||
|
||||
l.md.keepalive = mdutil.GetBool(md, "keepalive")
|
||||
l.md.keepaliveIdle = mdutil.GetDuration(md, "keepalive.idle")
|
||||
|
||||
@@ -2,6 +2,7 @@ package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -75,7 +76,13 @@ func (l *tunListener) Init(md mdata.Metadata) (err error) {
|
||||
func (l *tunListener) listenLoop(ready context.CancelCauseFunc) {
|
||||
for {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := func() error {
|
||||
err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in createTun: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ifce, name, ip, err := l.createTun()
|
||||
if err != nil {
|
||||
if ifce != nil {
|
||||
|
||||
@@ -52,8 +52,17 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.md.config.Net) > 0 {
|
||||
ipNet := l.md.config.Net[0]
|
||||
// Loop over every configured address so dual-stack (IPv4+IPv6) setups
|
||||
// install both addresses, selecting the netsh context by family: "ip"
|
||||
// for IPv4, "ipv6" for IPv6. A failing address is logged and skipped so
|
||||
// one bad entry doesn't prevent the rest from being applied; the first
|
||||
// successfully installed address is reported as the interface IP.
|
||||
//
|
||||
// Caveat: unlike Linux's netlink.AddrAdd, "netsh interface ip set
|
||||
// address" replaces rather than adds, so only a single IPv4 address is
|
||||
// supported (one IPv4 + one IPv6 works; multiple IPv4 addresses
|
||||
// silently overwrite).
|
||||
for _, ipNet := range l.md.config.Net {
|
||||
cmd := fmt.Sprintf("netsh interface ip set address name=%s "+
|
||||
"source=static addr=%s mask=%s gateway=none",
|
||||
name, ipNet.IP.String(), ipMask(ipNet.Mask))
|
||||
@@ -65,10 +74,19 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
|
||||
args := strings.Split(cmd, " ")
|
||||
if er := exec.Command(args[0], args[1:]...).Run(); er != nil {
|
||||
err = fmt.Errorf("%s: %v", cmd, er)
|
||||
return
|
||||
l.log.Errorf("%s: %v", cmd, er)
|
||||
continue
|
||||
}
|
||||
ip = ipNet.IP
|
||||
if ip == nil {
|
||||
ip = ipNet.IP
|
||||
}
|
||||
}
|
||||
|
||||
// If not a single address could be configured, surface an error rather
|
||||
// than returning a nil IP (and thus a nil peer address) to the caller.
|
||||
if ip == nil && len(l.md.config.Net) > 0 {
|
||||
err = fmt.Errorf("failed to configure any address on interface %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
if err = l.addRoutes(name, l.md.config.Gateway); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package tungo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -68,7 +69,13 @@ func (l *tunListener) Init(md mdata.Metadata) (err error) {
|
||||
func (l *tunListener) listenLoop(ready context.CancelCauseFunc) {
|
||||
for {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := func() error {
|
||||
err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in createTun: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ifce, name, ip, err := l.createTun()
|
||||
if err != nil {
|
||||
if ifce != nil {
|
||||
|
||||
@@ -56,8 +56,17 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.md.config.Net) > 0 {
|
||||
ipNet := l.md.config.Net[0]
|
||||
// Loop over every configured address so dual-stack (IPv4+IPv6) setups
|
||||
// install both addresses, selecting the netsh context by family: "ip"
|
||||
// for IPv4, "ipv6" for IPv6. A failing address is logged and skipped so
|
||||
// one bad entry doesn't prevent the rest from being applied; the first
|
||||
// successfully installed address is reported as the interface IP.
|
||||
//
|
||||
// Caveat: unlike Linux's netlink.AddrAdd, "netsh interface ip set
|
||||
// address" replaces rather than adds, so only a single IPv4 address is
|
||||
// supported (one IPv4 + one IPv6 works; multiple IPv4 addresses
|
||||
// silently overwrite).
|
||||
for _, ipNet := range l.md.config.Net {
|
||||
cmd := fmt.Sprintf("netsh interface ip set address name=%s "+
|
||||
"source=static addr=%s mask=%s",
|
||||
name, ipNet.IP.String(), ipMask(ipNet.Mask))
|
||||
@@ -73,10 +82,19 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net.
|
||||
l.log.Debugf("%s: %s", cmd, windows.ByteSliceToString(output))
|
||||
}
|
||||
if er != nil {
|
||||
err = fmt.Errorf("%s: %v", cmd, er)
|
||||
return
|
||||
l.log.Errorf("%s: %v", cmd, er)
|
||||
continue
|
||||
}
|
||||
ip = ipNet.IP
|
||||
if ip == nil {
|
||||
ip = ipNet.IP
|
||||
}
|
||||
}
|
||||
|
||||
// If not a single address could be configured, surface an error rather
|
||||
// than returning a nil IP (and thus a nil peer address) to the caller.
|
||||
if ip == nil && len(l.md.config.Net) > 0 {
|
||||
err = fmt.Errorf("failed to configure any address on interface %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
if err = l.addRoutes(name, l.md.config.Gateway); err != nil {
|
||||
|
||||
+25
-1
@@ -2,9 +2,11 @@ package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/limiter"
|
||||
@@ -85,6 +87,12 @@ func (l *wsListener) Init(md md.Metadata) (err error) {
|
||||
Addr: l.options.Addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: l.md.readHeaderTimeout,
|
||||
// Route HTTP server errors (TLS handshake failures, malformed
|
||||
// requests) to the GOST logger instead of the default stderr
|
||||
// output via log.Printf. Without this, network scanners and
|
||||
// protocol filters probing the WSS port produce uncontrolled
|
||||
// noise: "http: TLS handshake error from ...".
|
||||
ErrorLog: stdlog.New(&logWriter{log: l.log}, "", 0),
|
||||
}
|
||||
|
||||
l.cqueue = make(chan net.Conn, l.md.backlog)
|
||||
@@ -186,7 +194,9 @@ func (l *wsListener) upgrade(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
conn, err := l.upgrader.Upgrade(w, r, l.md.header)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
// Upgrade failures (plain HTTP requests, websocket
|
||||
// handshake errors) are operational noise — log at Warn.
|
||||
log.Warn(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,3 +218,17 @@ func (l *wsListener) upgrade(w http.ResponseWriter, r *http.Request) {
|
||||
log.Warnf("connection queue is full, client %s discarded", conn.RemoteAddr())
|
||||
}
|
||||
}
|
||||
|
||||
// logWriter adapts a GOST logger to the io.Writer interface required by
|
||||
// http.Server.ErrorLog. It writes each log entry as a Debug-level message
|
||||
// so that HTTP server errors (TLS handshake failures, malformed requests)
|
||||
// are captured in GOST's logging pipeline instead of escaping to stderr
|
||||
// via log.Printf.
|
||||
type logWriter struct {
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(p []byte) (n int, err error) {
|
||||
w.log.Debug(strings.TrimSpace(string(p)))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
+25
-2
@@ -4,6 +4,8 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-gost/core/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,10 +32,31 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
defaultMetrics metrics.Metrics = NewMetrics()
|
||||
enabled atomic.Bool
|
||||
defaultMetrics metrics.Metrics
|
||||
defaultRegistry *prometheus.Registry
|
||||
enabled atomic.Bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
defaultRegistry = prometheus.NewRegistry()
|
||||
|
||||
// Register Go and process collectors on our custom registry so the metrics
|
||||
// endpoint is self-contained. The process collector is platform-specific:
|
||||
// Linux/Windows use the standard procfs/Win32-based collector; all other
|
||||
// platforms (FreeBSD, Darwin, etc.) use a gopsutil-based collector.
|
||||
defaultRegistry.MustRegister(collectors.NewGoCollector())
|
||||
registerProcessCollector(defaultRegistry)
|
||||
|
||||
defaultMetrics = NewMetrics(defaultRegistry)
|
||||
}
|
||||
|
||||
// Registry returns the custom Prometheus registry that holds all GOST metrics
|
||||
// plus the Go and process collectors. The metrics HTTP endpoint should serve
|
||||
// from this registry rather than prometheus.DefaultGatherer.
|
||||
func Registry() *prometheus.Registry {
|
||||
return defaultRegistry
|
||||
}
|
||||
|
||||
// Enable enables or disables metrics collection globally. When disabled, all
|
||||
// GetCounter, GetGauge, and GetObserver calls return noop implementations.
|
||||
func Enable(b bool) {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//go:build !linux && !windows
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
psutil "github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
// gopsutilProcessCollector implements prometheus.Collector using gopsutil/v3
|
||||
// for cross-platform process metrics on non-Linux, non-Windows systems
|
||||
// (FreeBSD, Darwin, etc.) where procfs is unavailable.
|
||||
type gopsutilProcessCollector struct {
|
||||
pidFn func() (int, error)
|
||||
cpuTotal *prometheus.Desc
|
||||
openFDs *prometheus.Desc
|
||||
maxFDs *prometheus.Desc
|
||||
vsize *prometheus.Desc
|
||||
maxVsize *prometheus.Desc
|
||||
rss *prometheus.Desc
|
||||
startTime *prometheus.Desc
|
||||
reportErrors bool
|
||||
}
|
||||
|
||||
func getPID() (int, error) { return os.Getpid(), nil }
|
||||
|
||||
func newGopsutilProcessCollector() prometheus.Collector {
|
||||
return &gopsutilProcessCollector{
|
||||
pidFn: getPID,
|
||||
cpuTotal: prometheus.NewDesc(
|
||||
"process_cpu_seconds_total",
|
||||
"Total user and system CPU time spent in seconds.",
|
||||
nil, nil,
|
||||
),
|
||||
openFDs: prometheus.NewDesc(
|
||||
"process_open_fds",
|
||||
"Number of open file descriptors.",
|
||||
nil, nil,
|
||||
),
|
||||
maxFDs: prometheus.NewDesc(
|
||||
"process_max_fds",
|
||||
"Maximum number of open file descriptors.",
|
||||
nil, nil,
|
||||
),
|
||||
vsize: prometheus.NewDesc(
|
||||
"process_virtual_memory_bytes",
|
||||
"Virtual memory size in bytes.",
|
||||
nil, nil,
|
||||
),
|
||||
maxVsize: prometheus.NewDesc(
|
||||
"process_virtual_memory_max_bytes",
|
||||
"Maximum amount of virtual memory available in bytes.",
|
||||
nil, nil,
|
||||
),
|
||||
rss: prometheus.NewDesc(
|
||||
"process_resident_memory_bytes",
|
||||
"Resident memory size in bytes.",
|
||||
nil, nil,
|
||||
),
|
||||
startTime: prometheus.NewDesc(
|
||||
"process_start_time_seconds",
|
||||
"Start time of the process since unix epoch in seconds.",
|
||||
nil, nil,
|
||||
),
|
||||
reportErrors: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gopsutilProcessCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.cpuTotal
|
||||
ch <- c.openFDs
|
||||
ch <- c.maxFDs
|
||||
ch <- c.vsize
|
||||
ch <- c.maxVsize
|
||||
ch <- c.rss
|
||||
ch <- c.startTime
|
||||
}
|
||||
|
||||
func (c *gopsutilProcessCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
pid, err := c.pidFn()
|
||||
if err != nil {
|
||||
c.reportError(ch, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := psutil.NewProcess(int32(pid))
|
||||
if err != nil {
|
||||
c.reportError(ch, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Memory metrics: RSS (resident) and VMS (virtual).
|
||||
if mem, err := p.MemoryInfo(); err == nil {
|
||||
ch <- prometheus.MustNewConstMetric(c.rss, prometheus.GaugeValue, float64(mem.RSS))
|
||||
ch <- prometheus.MustNewConstMetric(c.vsize, prometheus.GaugeValue, float64(mem.VMS))
|
||||
} else {
|
||||
c.reportError(ch, c.rss, err)
|
||||
}
|
||||
|
||||
// CPU time.
|
||||
if times, err := p.Times(); err == nil {
|
||||
cpuSeconds := times.User + times.System
|
||||
ch <- prometheus.MustNewConstMetric(c.cpuTotal, prometheus.CounterValue, cpuSeconds)
|
||||
} else {
|
||||
c.reportError(ch, c.cpuTotal, err)
|
||||
}
|
||||
|
||||
// Open file descriptors.
|
||||
if fds, err := p.NumFDs(); err == nil {
|
||||
ch <- prometheus.MustNewConstMetric(c.openFDs, prometheus.GaugeValue, float64(fds))
|
||||
} else {
|
||||
c.reportError(ch, c.openFDs, err)
|
||||
}
|
||||
|
||||
// Max file descriptors — not reliably available via gopsutil on all platforms.
|
||||
// Try RLIMIT_NOFILE via gopsutil's RlimitUsage.
|
||||
if rlimits, err := p.RlimitUsage(false); err == nil && len(rlimits) > 0 {
|
||||
for _, rl := range rlimits {
|
||||
switch rl.Resource {
|
||||
case psutil.RLIMIT_NOFILE:
|
||||
ch <- prometheus.MustNewConstMetric(c.maxFDs, prometheus.GaugeValue, float64(rl.Hard))
|
||||
case psutil.RLIMIT_AS:
|
||||
ch <- prometheus.MustNewConstMetric(c.maxVsize, prometheus.GaugeValue, float64(rl.Hard))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process start time.
|
||||
if ct, err := p.CreateTime(); err == nil {
|
||||
createTime := time.Unix(ct/1000, (ct%1000)*1e6)
|
||||
ch <- prometheus.MustNewConstMetric(c.startTime, prometheus.GaugeValue, float64(createTime.Unix()))
|
||||
} else {
|
||||
c.reportError(ch, c.startTime, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gopsutilProcessCollector) reportError(ch chan<- prometheus.Metric, desc *prometheus.Desc, err error) {
|
||||
if !c.reportErrors {
|
||||
return
|
||||
}
|
||||
if desc == nil {
|
||||
desc = prometheus.NewInvalidDesc(err)
|
||||
}
|
||||
ch <- prometheus.NewInvalidMetric(desc, err)
|
||||
}
|
||||
|
||||
// registerProcessCollector registers a gopsutil-based process metrics collector
|
||||
// for platforms where procfs is unavailable.
|
||||
func registerProcessCollector(reg *prometheus.Registry) {
|
||||
reg.MustRegister(newGopsutilProcessCollector())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build linux || windows
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
)
|
||||
|
||||
// registerProcessCollector registers the standard prometheus ProcessCollector
|
||||
// which uses procfs on Linux and Win32 syscalls on Windows.
|
||||
func registerProcessCollector(reg *prometheus.Registry) {
|
||||
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
||||
}
|
||||
+10
-5
@@ -16,8 +16,13 @@ type promMetrics struct {
|
||||
}
|
||||
|
||||
// NewMetrics returns a Prometheus-based Metrics implementation. All metrics are
|
||||
// automatically registered with the default Prometheus registry.
|
||||
func NewMetrics() metrics.Metrics {
|
||||
// registered with the given registry. Use nil to register with the default
|
||||
// Prometheus registry.
|
||||
func NewMetrics(reg *prometheus.Registry) metrics.Metrics {
|
||||
if reg == nil {
|
||||
reg = prometheus.DefaultRegisterer.(*prometheus.Registry)
|
||||
}
|
||||
|
||||
host, _ := os.Hostname()
|
||||
m := &promMetrics{
|
||||
host: host,
|
||||
@@ -95,13 +100,13 @@ func NewMetrics() metrics.Metrics {
|
||||
},
|
||||
}
|
||||
for k := range m.gauges {
|
||||
prometheus.MustRegister(m.gauges[k])
|
||||
reg.MustRegister(m.gauges[k])
|
||||
}
|
||||
for k := range m.counters {
|
||||
prometheus.MustRegister(m.counters[k])
|
||||
reg.MustRegister(m.counters[k])
|
||||
}
|
||||
for k := range m.histograms {
|
||||
prometheus.MustRegister(m.histograms[k])
|
||||
reg.MustRegister(m.histograms[k])
|
||||
}
|
||||
|
||||
return m
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
xmetrics "github.com/go-gost/x/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -46,6 +49,8 @@ type metricService struct {
|
||||
}
|
||||
|
||||
// NewService creates a metrics Service that exposes Prometheus metrics over HTTP.
|
||||
// It serves from the GOST custom registry rather than the default one so that
|
||||
// process metrics are available on all platforms.
|
||||
func NewService(network, addr string, opts ...Option) (service.Service, error) {
|
||||
if network == "" {
|
||||
network = "tcp"
|
||||
@@ -82,7 +87,12 @@ func NewService(network, addr string, opts ...Option) (service.Service, error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
promhttp.Handler().ServeHTTP(w, r)
|
||||
|
||||
reg := xmetrics.Registry()
|
||||
if reg == nil {
|
||||
reg = prometheus.DefaultRegisterer.(*prometheus.Registry)
|
||||
}
|
||||
promhttp.HandlerFor(reg, promhttp.HandlerOpts{}).ServeHTTP(w, r)
|
||||
}))
|
||||
return &metricService{
|
||||
s: &http.Server{
|
||||
|
||||
@@ -2,6 +2,8 @@ package wrapper
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
xmetrics "github.com/go-gost/x/metrics"
|
||||
)
|
||||
|
||||
type listener struct {
|
||||
@@ -10,10 +12,11 @@ type listener struct {
|
||||
}
|
||||
|
||||
// WrapListener wraps a net.Listener so that all accepted connections are
|
||||
// automatically wrapped with WrapConn for metrics tracking. If ln is nil,
|
||||
// nil is returned.
|
||||
// automatically wrapped with WrapConn for metrics tracking. If metrics are
|
||||
// not enabled, ln is returned unchanged to allow splice(2) optimization
|
||||
// on bare *net.TCPConn. If ln is nil, nil is returned.
|
||||
func WrapListener(service string, ln net.Listener) net.Listener {
|
||||
if ln == nil {
|
||||
if ln == nil || !xmetrics.IsEnabled() {
|
||||
return ln
|
||||
}
|
||||
return &listener{
|
||||
|
||||
@@ -10,6 +10,23 @@ import (
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
)
|
||||
|
||||
// MetadataRecorder wraps a Recorder and automatically appends metadata
|
||||
// to every Record call.
|
||||
type MetadataRecorder struct {
|
||||
recorder.Recorder
|
||||
Metadata any
|
||||
}
|
||||
|
||||
func (r *MetadataRecorder) Record(ctx context.Context, b []byte, opts ...recorder.RecordOption) error {
|
||||
if r.Recorder == nil {
|
||||
return nil
|
||||
}
|
||||
if r.Metadata == nil {
|
||||
return r.Recorder.Record(ctx, b, opts...)
|
||||
}
|
||||
return r.Recorder.Record(ctx, b, append(opts, recorder.MetadataRecordOption(r.Metadata))...)
|
||||
}
|
||||
|
||||
const (
|
||||
RecorderServiceHandler = "recorder.service.handler"
|
||||
RecorderServiceHandlerSerial = "recorder.service.handler.serial"
|
||||
|
||||
Reference in New Issue
Block a user