From 61c13586e915b50ebbc3fbfb77e13b00c35a108b Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 11 Mar 2026 02:15:52 +0800 Subject: [PATCH] feat: add `ping-interval` to `grpc-opts` --- adapter/outbound/trojan.go | 20 ++++++------- adapter/outbound/vless.go | 18 ++++++------ adapter/outbound/vmess.go | 19 ++++++------ docs/config.yaml | 3 ++ transport/gun/gun.go | 43 +++++++++++++++------------- transport/gun/transport.go | 13 +++++---- transport/gun/transport_close.go | 2 +- transport/trusttunnel/force_close.go | 2 +- 8 files changed, 62 insertions(+), 58 deletions(-) diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index b691ff00..497ff05b 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -27,8 +27,7 @@ type Trojan struct { hexPassword [trojan.KeyLength]byte // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -178,7 +177,7 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con var c net.Conn // gun transport if t.gunTransport != nil { - c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig) + c, err = t.gunTransport.Dial() } else { c, err = t.dialer.DialContext(ctx, "tcp", t.addr) } @@ -206,7 +205,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) var c net.Conn // grpc transport if t.gunTransport != nil { - c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig) + c, err = t.gunTransport.Dial() } else { c, err = t.dialer.DialContext(ctx, "tcp", t.addr) } @@ -317,13 +316,14 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { Reality: t.realityConfig, } - t.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) - - t.gunConfig = &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.SNI, + gunConfig := &gun.Config{ + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.SNI, + PingInterval: option.GrpcOpts.PingInterval, } + + t.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return t, nil diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index bd060b18..78ccb667 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -33,8 +33,7 @@ type Vless struct { encryption *encryption.ClientInstance // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -234,7 +233,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -260,7 +259,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -431,9 +430,10 @@ func NewVless(option VlessOption) (*Vless, error) { } gunConfig := &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.ServerName, + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.ServerName, + PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr @@ -457,9 +457,7 @@ func NewVless(option VlessOption) (*Vless, error) { } } - v.gunConfig = gunConfig - - v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) + v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return v, nil diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 29ed63fd..0de06b7c 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -34,8 +34,7 @@ type Vmess struct { option *VmessOption // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -86,6 +85,7 @@ type HTTP2Options struct { type GrpcOptions struct { GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"` + PingInterval int `proxy:"ping-interval,omitempty"` } type WSOptions struct { @@ -295,7 +295,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -318,7 +318,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -437,9 +437,10 @@ func NewVmess(option VmessOption) (*Vmess, error) { } gunConfig := &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.ServerName, + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.ServerName, + PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr @@ -463,9 +464,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { } } - v.gunConfig = gunConfig - - v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) + v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return v, nil diff --git a/docs/config.yaml b/docs/config.yaml index 09467a11..efaa9ede 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -669,6 +669,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 # ip-version: ipv4 # vless @@ -759,6 +760,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "grpc" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE @@ -830,6 +832,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 - name: trojan-ws server: server diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 27c0cbbb..65313df9 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -59,9 +59,10 @@ type Conn struct { } type Config struct { - ServiceName string - UserAgent string - Host string + ServiceName string + UserAgent string + Host string + PingInterval int } func (g *Conn) initReader() { @@ -246,7 +247,7 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap { +func NewTransport(dialFn DialFn, tlsConfig *vmess.TLSConfig, gunCfg *Config) *Transport { dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() @@ -288,14 +289,16 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap { DialTLSContext: dialFunc, AllowHTTP: false, DisableCompression: true, + ReadIdleTimeout: time.Duration(gunCfg.PingInterval) * time.Second, // If zero, no health check is performed PingTimeout: 0, } ctx, cancel := context.WithCancel(context.Background()) - wrap := &TransportWrap{ - Http2Transport: transport, - ctx: ctx, - cancel: cancel, + wrap := &Transport{ + transport: transport, + cfg: gunCfg, + ctx: ctx, + cancel: cancel, } return wrap } @@ -307,18 +310,18 @@ func ServiceNameToPath(serviceName string) string { return "/" + serviceName + "/Tun" } -func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, error) { +func (t *Transport) Dial() (net.Conn, error) { serviceName := "GunService" - if cfg.ServiceName != "" { - serviceName = cfg.ServiceName + if t.cfg.ServiceName != "" { + serviceName = t.cfg.ServiceName } path := ServiceNameToPath(serviceName) reader, writer := io.Pipe() header := defaultHeader.Clone() - if cfg.UserAgent != "" { - header.Set("User-Agent", cfg.UserAgent) + if t.cfg.UserAgent != "" { + header.Set("User-Agent", t.cfg.UserAgent) } request := &http.Request{ @@ -326,17 +329,17 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er Body: reader, URL: &url.URL{ Scheme: "https", - Host: cfg.Host, + Host: t.cfg.Host, Path: path, // for unescape path - Opaque: "//" + cfg.Host + path, + Opaque: "//" + t.cfg.Host + path, }, Proto: "HTTP/2", ProtoMajor: 2, ProtoMinor: 0, Header: header, } - request = request.WithContext(transport.ctx) + request = request.WithContext(t.ctx) conn := &Conn{ initFn: func() (io.ReadCloser, NetAddr, error) { @@ -348,7 +351,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er }, } request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace)) - response, err := transport.RoundTrip(request) + response, err := t.transport.RoundTrip(request) if err != nil { return nil, nAddr, err } @@ -361,13 +364,13 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er return conn, nil } -func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, cfg *Config) (net.Conn, error) { +func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig) - c, err := StreamGunWithTransport(transport, cfg) + transport := NewTransport(dialFn, tlsConfig, gunCfg) + c, err := transport.Dial() if err != nil { return nil, err } diff --git a/transport/gun/transport.go b/transport/gun/transport.go index 4b9da971..80c0af15 100644 --- a/transport/gun/transport.go +++ b/transport/gun/transport.go @@ -10,17 +10,18 @@ import ( "github.com/metacubex/http" ) -type TransportWrap struct { - *http.Http2Transport +type Transport struct { + transport *http.Http2Transport + cfg *Config ctx context.Context cancel context.CancelFunc closeOnce sync.Once } -func (tw *TransportWrap) Close() error { - tw.closeOnce.Do(func() { - tw.cancel() - CloseTransport(tw.Http2Transport) +func (t *Transport) Close() error { + t.closeOnce.Do(func() { + t.cancel() + CloseHttp2Transport(t.transport) }) return nil } diff --git a/transport/gun/transport_close.go b/transport/gun/transport_close.go index 44fefd7c..add4906e 100644 --- a/transport/gun/transport_close.go +++ b/transport/gun/transport_close.go @@ -44,7 +44,7 @@ func closeClientConn(cc *http.Http2ClientConn) { // like forceCloseConn() in htt _ = cc.Close() } -func CloseTransport(tr *http.Http2Transport) { +func CloseHttp2Transport(tr *http.Http2Transport) { connPool := transportConnPool(tr) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p.mu.Lock() diff --git a/transport/trusttunnel/force_close.go b/transport/trusttunnel/force_close.go index 3253b6c6..d34b9376 100644 --- a/transport/trusttunnel/force_close.go +++ b/transport/trusttunnel/force_close.go @@ -11,7 +11,7 @@ func forceCloseAllConnections(roundTripper RoundTripper) { roundTripper.CloseIdleConnections() switch tr := roundTripper.(type) { case *http.Http2Transport: - gun.CloseTransport(tr) + gun.CloseHttp2Transport(tr) case *http3.Transport: _ = tr.Close() }