fix(dialer): prevent hang on HTTP+ICMP by enabling QUIC keepalive by default

The ICMP dialer required explicit keepalive:true to send QUIC PING frames,
while the regular QUIC dialer enabled keepalive by default (10s period).
Without PING frames, quic-go's 30s idle timeout silently killed connections,
causing HTTP+ICMP tunnels to hang after periods of inactivity.

Fixes go-gost/gost#352

Also in this commit:
- Release session mutex before calling GetConn() in both ICMP and QUIC
  dialers, so a dead session doesn't block all Dial() calls while
  OpenStreamSync blocks
- Add 30s timeout context to OpenStreamSync to prevent indefinite blocking
  if the QUIC session dies between liveness check and stream open
- Guard session cache deletion with pointer identity check to avoid
  evicting a newly created session during a race
This commit is contained in:
ginuerzh
2026-06-21 10:09:34 +08:00
parent 37bc81576c
commit 9fab332772
5 changed files with 36 additions and 10 deletions
+4 -1
View File
@@ -3,6 +3,7 @@ package quic
import ( import (
"context" "context"
"net" "net"
"time"
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
) )
@@ -12,7 +13,9 @@ type quicSession struct {
} }
func (session *quicSession) GetConn() (*quicConn, error) { 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 { if err != nil {
return nil, err return nil, err
} }
+8 -2
View File
@@ -75,7 +75,6 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
} }
d.sessionMutex.Lock() d.sessionMutex.Lock()
defer d.sessionMutex.Unlock()
session, ok := d.sessions[addr] session, ok := d.sessions[addr]
if session != nil && session.IsClosed() { 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", "") pc, err = icmp_pkg.ListenPacket("ip4:icmp", "")
} }
if err != nil { if err != nil {
d.sessionMutex.Unlock()
return return
} }
@@ -109,16 +109,22 @@ func (d *icmpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
if err != nil { if err != nil {
d.logger.Error(err) d.logger.Error(err)
pc.Close() pc.Close()
d.sessionMutex.Unlock()
return nil, err return nil, err
} }
d.sessions[addr] = session d.sessions[addr] = session
} }
d.sessionMutex.Unlock()
conn, err = session.GetConn() conn, err = session.GetConn()
if err != nil { if err != nil {
d.sessionMutex.Lock()
if d.sessions[addr] == session {
delete(d.sessions, addr)
}
session.Close() session.Close()
delete(d.sessions, addr) d.sessionMutex.Unlock()
return nil, err return nil, err
} }
+11 -4
View File
@@ -14,14 +14,21 @@ type metadata struct {
} }
func (d *icmpDialer) parseMetadata(md mdata.Metadata) (err error) { func (d *icmpDialer) parseMetadata(md mdata.Metadata) (err error) {
if mdutil.GetBool(md, "keepalive") { const (
d.md.keepAlivePeriod = mdutil.GetDuration(md, "ttl") 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 { if d.md.keepAlivePeriod <= 0 {
d.md.keepAlivePeriod = 10 * time.Second d.md.keepAlivePeriod = 10 * time.Second
} }
} }
d.md.handshakeTimeout = mdutil.GetDuration(md, "handshakeTimeout") d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
d.md.maxIdleTimeout = mdutil.GetDuration(md, "maxIdleTimeout") d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout)
return return
} }
+4 -1
View File
@@ -3,6 +3,7 @@ package quic
import ( import (
"context" "context"
"net" "net"
"time"
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
) )
@@ -12,7 +13,9 @@ type quicSession struct {
} }
func (session *quicSession) GetConn() (*quicConn, error) { 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 { if err != nil {
return nil, err return nil, err
} }
+9 -2
View File
@@ -58,7 +58,6 @@ func (d *quicDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
} }
d.sessionMutex.Lock() d.sessionMutex.Lock()
defer d.sessionMutex.Unlock()
session, ok := d.sessions[addr] session, ok := d.sessions[addr]
if session != nil && session.IsClosed() { 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", "") c, err := options.Dialer.Dial(ctx, "udp", "")
if err != nil { if err != nil {
d.sessionMutex.Unlock()
return nil, err return nil, err
} }
pc, ok := c.(net.PacketConn) pc, ok := c.(net.PacketConn)
if !ok { if !ok {
c.Close() c.Close()
d.sessionMutex.Unlock()
return nil, errors.New("quic: wrong connection type") 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 { if err != nil {
d.logger.Error(err) d.logger.Error(err)
pc.Close() pc.Close()
d.sessionMutex.Unlock()
return nil, err return nil, err
} }
d.sessions[addr] = session d.sessions[addr] = session
} }
d.sessionMutex.Unlock()
conn, err = session.GetConn() conn, err = session.GetConn()
if err != nil { if err != nil {
d.sessionMutex.Lock()
if d.sessions[addr] == session {
delete(d.sessions, addr)
}
session.Close() session.Close()
delete(d.sessions, addr) d.sessionMutex.Unlock()
return nil, err return nil, err
} }