Add SSH keepalive and auto-reconnection to prevent idle disconnections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis Montañes Ojados
2026-02-09 01:11:45 +01:00
parent 34af75aaa1
commit c83fa3530d

View File

@@ -31,6 +31,7 @@ type Client struct {
Events chan string // Channel to send logs/events to TUI Events chan string // Channel to send logs/events to TUI
Metrics chan int64 // Channel to send bytes transferred Metrics chan int64 // Channel to send bytes transferred
PublicURL string // PublicURL is the URL accessible from the internet PublicURL string // PublicURL is the URL accessible from the internet
stopKeepAlive chan struct{} // Signal to stop keepalive goroutine
} }
func NewClient(serverAddr, localPort, authToken, hostHeader string, localHTTPS bool) *Client { func NewClient(serverAddr, localPort, authToken, hostHeader string, localHTTPS bool) *Client {
@@ -45,7 +46,7 @@ func NewClient(serverAddr, localPort, authToken, hostHeader string, localHTTPS b
} }
} }
func (c *Client) Start() error { func (c *Client) connect() error {
config := &ssh.ClientConfig{ config := &ssh.ClientConfig{
User: "grokway", User: "grokway",
Auth: []ssh.AuthMethod{ Auth: []ssh.AuthMethod{
@@ -93,18 +94,86 @@ func (c *Client) Start() error {
c.PublicURL = fmt.Sprintf("https://%s.%s", slug, host) c.PublicURL = fmt.Sprintf("https://%s.%s", slug, host)
c.Events <- fmt.Sprintf("Tunnel established! Public URL: %s", c.PublicURL) c.Events <- fmt.Sprintf("Tunnel established! Public URL: %s", c.PublicURL)
go c.acceptLoop() // Start SSH keepalive to prevent idle disconnection
c.stopKeepAlive = make(chan struct{})
go c.keepAlive()
return nil return nil
} }
func (c *Client) Start() error {
if err := c.connect(); err != nil {
return err
}
go c.acceptLoop()
return nil
}
// keepAlive sends periodic SSH keepalive requests to prevent
// firewalls/NATs from dropping idle connections.
func (c *Client) keepAlive() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-c.stopKeepAlive:
return
case <-ticker.C:
if c.SSHClient != nil {
_, _, err := c.SSHClient.SendRequest("keepalive@openssh.com", true, nil)
if err != nil {
logToFile(fmt.Sprintf("Keepalive failed: %v", err))
return
}
}
}
}
}
func (c *Client) closeConnection() {
if c.stopKeepAlive != nil {
select {
case <-c.stopKeepAlive:
// Already closed
default:
close(c.stopKeepAlive)
}
}
if c.Listener != nil {
c.Listener.Close()
}
if c.SSHClient != nil {
c.SSHClient.Close()
}
}
func (c *Client) acceptLoop() { func (c *Client) acceptLoop() {
for { for {
remoteConn, err := c.Listener.Accept() remoteConn, err := c.Listener.Accept()
if err != nil { if err != nil {
c.Events <- fmt.Sprintf("Accept error: %s", err) c.Events <- fmt.Sprintf("Connection lost: %s", err)
c.closeConnection()
// Reconnect with backoff
delay := 2 * time.Second
maxDelay := 60 * time.Second
for {
c.Events <- fmt.Sprintf("Reconnecting in %s...", delay)
time.Sleep(delay)
if err := c.connect(); err != nil {
c.Events <- fmt.Sprintf("Reconnection failed: %s", err)
delay *= 2
if delay > maxDelay {
delay = maxDelay
}
continue
}
c.Events <- "Reconnected successfully!"
break break
} }
continue
}
c.Events <- "New Request received" c.Events <- "New Request received"
go c.handleConnection(remoteConn) go c.handleConnection(remoteConn)