Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions server/remote/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import (
"github.com/hashicorp/yamux"
)

// bad registration token or sponsor token
var errCredentialsRejected = errors.New("remote access rejected")

const configRetryInterval = 15 * time.Minute

const minUptime = 5 * time.Second

// Tunnel manages a WebSocket+yamux tunnel to the cloud proxy.
type Tunnel struct {
tunnelURL string
Expand Down Expand Up @@ -63,27 +70,44 @@ func (t *Tunnel) run() {
t.log.ERROR.Printf("tunnel: %v", err)
}

// reset backoff after successful connection
// reset backoff only after a session that stayed connected
if ok {
bo.Reset()
}

wait := bo.NextBackOff()
// rejected credentials will not self-heal; retry slowly
if errors.Is(err, errCredentialsRejected) {
wait = configRetryInterval
Comment thread
naltatis marked this conversation as resolved.
Outdated
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

select {
case <-ctx.Done():
return
case <-time.After(bo.NextBackOff()):
case <-time.After(wait):
}
}
}

func (t *Tunnel) connect(ctx context.Context) (bool, error) {
conn, _, err := websocket.Dial(ctx, t.tunnelURL, &websocket.DialOptions{
conn, resp, err := websocket.Dial(ctx, t.tunnelURL, &websocket.DialOptions{
HTTPHeader: http.Header{
"Authorization": []string{"Bearer " + t.token},
"X-Sponsor-Token": []string{sponsor.Token},
},
})
if err != nil {
// rejected before the websocket upgrade; resp carries the HTTP status
if resp != nil {
switch resp.StatusCode {
case http.StatusConflict:
return false, errors.New("another evcc instance is already connected with these credentials")
case http.StatusUnauthorized:
return false, fmt.Errorf("%w (access revoked or sponsor token missing)", errCredentialsRejected)
case http.StatusForbidden:
return false, fmt.Errorf("%w (sponsor token invalid or expired)", errCredentialsRejected)
}
}
return false, fmt.Errorf("websocket dial: %w", err)
}

Expand All @@ -99,6 +123,7 @@ func (t *Tunnel) connect(ctx context.Context) (bool, error) {
}

t.changeState(session, nil)
start := time.Now()

// accept streams from the proxy
srv := &http.Server{
Expand All @@ -109,7 +134,8 @@ func (t *Tunnel) connect(ctx context.Context) (bool, error) {
t.changeState(nil, err)
}

return true, nil
// only a sustained connection resets backoff; an instant close keeps backing off
return time.Since(start) >= minUptime, nil
}

func (t *Tunnel) changeState(session *yamux.Session, err error) {
Expand Down
Loading