Remote access: show tunnel connect errors, self-recover after offline boot (#32511)

This commit is contained in:
Michael Geers 2026-08-04 12:57:07 +02:00 • committed by GitHub
parent 37fa159d47
commit addbda0c43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 67 additions and 2 deletions

View file

@ -4127,6 +4127,10 @@
"additionalProperties": {
"type": "string"
}
},
"error": {
"description": "Last connection error.",
"type": "string"
}
},
"required": [

View file

@ -1586,6 +1586,9 @@ components:
type: object
additionalProperties:
type: string
error:
description: Last connection error.
type: string
required:
- connected
- loginBlocked

View file

@ -1,6 +1,7 @@
package remote
import (
"errors"
"fmt"
"net/http"
"sync"
@ -34,6 +35,7 @@ type Remote struct {
publisher chan<- util.Param
lastSeen map[string]time.Time // persisted: username → last activity
connected map[string]int // in-memory: active connection count per user
lastError error // last connect/registration error, published to UI
}
// New creates a new Remote manager, loads persisted settings, and connects if enabled.
@ -93,8 +95,10 @@ func (r *Remote) Enabled() bool {
}
func (r *Remote) connect() {
if !sponsor.IsAuthorizedForApi() {
r.log.WARN.Println("remote access requires a sponsor token")
if sponsor.Token == "" {
msg := "remote access requires a sponsor token"
r.log.WARN.Println(msg)
r.setError(errors.New(msg))
return
}
@ -105,10 +109,13 @@ func (r *Remote) connect() {
if token == "" {
if err := r.register(); err != nil {
r.log.ERROR.Printf("registration failed: %v", err)
r.setError(fmt.Errorf("registration failed: %w", err))
return
}
}
r.setError(nil)
r.log.INFO.Printf("remote access via %s", r.settings.URL)
tunnel := NewTunnel(r.settings.TunnelURL, r.settings.Token, r.httpHandler, r.Authenticate, r.TrackActivity, r.log, r.publish)
@ -129,6 +136,16 @@ func (r *Remote) disconnect() {
r.tunnel.Close()
r.tunnel = nil
}
r.lastError = nil
}
// setError records a connect error and publishes it to the UI.
func (r *Remote) setError(err error) {
r.mu.Lock()
r.lastError = err
r.mu.Unlock()
r.publish()
}
type registerRequest struct {
@ -192,6 +209,16 @@ func (r *Remote) ConfigStatus() globalconfig.ConfigStatus {
connected := r.tunnel != nil && r.tunnel.IsConnected()
loginBlocked := r.tunnel != nil && r.tunnel.LoginBlocked()
lastErr := r.lastError
if lastErr == nil && r.tunnel != nil {
lastErr = r.tunnel.Error()
}
var errMsg string
if lastErr != nil {
errMsg = lastErr.Error()
}
return globalconfig.ConfigStatus{
Config: struct {
Enabled bool `json:"enabled"`
@ -203,11 +230,13 @@ func (r *Remote) ConfigStatus() globalconfig.ConfigStatus {
URL string `json:"url,omitempty"`
LoginBlocked bool `json:"loginBlocked"`
LastSeen map[string]time.Time `json:"lastSeen,omitempty"`
Error string `json:"error,omitempty"`
}{
Connected: connected,
URL: r.settings.URL,
LoginBlocked: loginBlocked,
LastSeen: r.lastSeen,
Error: errMsg,
},
}
}

View file

@ -35,6 +35,7 @@ type Tunnel struct {
mu sync.Mutex
session *yamux.Session
lastErr error
}
// NewTunnel creates a new tunnel client.
@ -66,6 +67,7 @@ func (t *Tunnel) run() {
ok, err := t.connect(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
t.log.ERROR.Printf("tunnel: %v", err)
t.setError(err)
}
// rejected credentials will not self-heal; a new token requires a restart
@ -139,6 +141,9 @@ func (t *Tunnel) connect(ctx context.Context) (bool, error) {
func (t *Tunnel) changeState(session *yamux.Session, err error) {
t.mu.Lock()
t.session = session
if session != nil {
t.lastErr = nil
}
t.mu.Unlock()
if t.onStateChange != nil {
@ -156,6 +161,23 @@ func (t *Tunnel) changeState(session *yamux.Session, err error) {
}
}
func (t *Tunnel) setError(err error) {
t.mu.Lock()
t.lastErr = err
t.mu.Unlock()
if t.onStateChange != nil {
t.onStateChange()
}
}
// Error returns the last connection error, if any.
func (t *Tunnel) Error() error {
t.mu.Lock()
defer t.mu.Unlock()
return t.lastErr
}
// IsConnected returns whether the tunnel is currently connected.
func (t *Tunnel) IsConnected() bool {
t.mu.Lock()