@@ -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.
103110func 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