Skip to content

Commit 681c174

Browse files
authored
Merge pull request #1 from dc-holdings-spa/fix/wss-keepalive
fix(client): WSS ping keepalive — no more 2min timeout disconnects
2 parents 860fcd2 + c35fef1 commit 681c174

1 file changed

Lines changed: 33 additions & 1 deletion

File tree

client/main.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,44 @@ func runShovel(ctx context.Context, udpConn *net.UDPConn, wsURL string) {
9999
}
100100
}
101101

102+
// wssReadTimeout is how long we wait for any inbound frame (data or pong)
103+
// before declaring the connection dead. Must be > wssKeepaliveInterval.
104+
const (
105+
wssKeepaliveInterval = 45 * time.Second
106+
wssReadTimeout = 90 * time.Second
107+
)
108+
102109
// runOneTunnel runs the bidirectional shovel until either side dies, then returns.
103110
func runOneTunnel(ctx context.Context, udpConn *net.UDPConn, ws *websocket.Conn) {
104111
defer ws.Close()
105112
subCtx, cancel := context.WithCancel(ctx)
106113
defer cancel()
107114

115+
// Reset read deadline on every pong so the keepalive goroutine below
116+
// keeps the connection alive through Cloudflare's idle-timeout window.
117+
ws.SetPongHandler(func(string) error {
118+
return ws.SetReadDeadline(time.Now().Add(wssReadTimeout))
119+
})
120+
ws.SetReadDeadline(time.Now().Add(wssReadTimeout))
121+
122+
// Keepalive: send a WebSocket ping every 45 s. Cloudflare drops idle
123+
// WebSocket connections after ~100 s; 45 s leaves comfortable headroom.
124+
go func() {
125+
ticker := time.NewTicker(wssKeepaliveInterval)
126+
defer ticker.Stop()
127+
for {
128+
select {
129+
case <-ticker.C:
130+
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
131+
if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil {
132+
return
133+
}
134+
case <-subCtx.Done():
135+
return
136+
}
137+
}
138+
}()
139+
108140
// Track WG's local UDP source so we know where to deliver WSS->UDP replies.
109141
var wgPeer *net.UDPAddr
110142
peerCh := make(chan *net.UDPAddr, 1)
@@ -138,13 +170,13 @@ func runOneTunnel(ctx context.Context, udpConn *net.UDPConn, ws *websocket.Conn)
138170
go func() {
139171
defer cancel()
140172
for {
141-
ws.SetReadDeadline(time.Now().Add(2 * time.Minute))
142173
typ, data, err := ws.ReadMessage()
143174
if err != nil {
144175
log.Printf("[wss] read: %v", err)
145176
return
146177
}
147178
if typ != websocket.BinaryMessage {
179+
// ping/pong/text frames — pong handler already reset deadline
148180
continue
149181
}
150182
// Wait for WG peer to be known (it dials us with the first packet)

0 commit comments

Comments
 (0)