Remote access: show tunnel connect errors, self-recover after offline boot (#32511)
This commit is contained in:
parent
37fa159d47
commit
addbda0c43
6 changed files with 67 additions and 2 deletions
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue