diff --git a/VERSION.txt b/VERSION.txt index 6e8bf73..17e51c3 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/internal/agent/client.go b/internal/agent/client.go index 5c734d8..abaec96 100644 --- a/internal/agent/client.go +++ b/internal/agent/client.go @@ -131,9 +131,7 @@ func (a *Agent) Run(ctx context.Context) error { case <-ctx.Done(): return nil } - if backoff < 15*time.Second { - backoff *= 2 - } + backoff = min(backoff*2, retryCap) continue case proto.TransportAuto: log.Printf("websocket: %v - long polling, websocket again in a minute", err) @@ -167,6 +165,10 @@ func (a *Agent) applyTransport(v string) { func (a *Agent) longPoll(ctx context.Context, d time.Duration) { until := time.Now().Add(d) backoff := time.Second + // The server was out of reach (a network down, a machine asleep): once + // a poll gets through again, "auto" tries the WebSocket at once rather + // than at the end of the minute - it was the network, not the WebSocket. + lost := false for ctx.Err() == nil { if d > 0 && (time.Now().After(until) || a.mode() == proto.TransportWS) { return @@ -174,6 +176,21 @@ func (a *Agent) longPoll(ctx context.Context, d time.Duration) { if d == 0 && a.mode() != proto.TransportLP { return } + // Back from out of reach in "auto": a quick request tells when the + // server answers again, and the WebSocket is tried then; a poll + // would sit held on the server first. + if lost && d > 0 { + if a.reachable(ctx) { + return + } + select { + case <-time.After(backoff): + case <-ctx.Done(): + return + } + backoff = min(backoff*2, retryCap) + continue + } // In "auto" the minute is a minute: a poll still held then is cut // short (the server keeps its job for the next one). reqCtx, cancel := ctx, context.CancelFunc(func() {}) @@ -193,15 +210,32 @@ func (a *Agent) longPoll(ctx context.Context, d time.Duration) { case <-ctx.Done(): return } - if backoff < 15*time.Second { - backoff *= 2 - } + backoff = min(backoff*2, retryCap) + lost = true continue } backoff = time.Second } } +// reachable reports whether the server answers a quick request. +func (a *Agent) reachable(ctx context.Context) bool { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.Host+"/api/agent/config", nil) + a.authHeaders(req) + resp, err := a.http.Do(req) + if err != nil { + return false + } + resp.Body.Close() + return resp.StatusCode < 300 +} + +// retryCap bounds the wait between attempts to reach the server, so a +// machine back from sleep or a network back up is reconnected in seconds. +const retryCap = 5 * time.Second + func (a *Agent) enroll(ctx context.Context) error { host, _ := os.Hostname() body, _ := json.Marshal(proto.EnrollRequest{ diff --git a/internal/agent/tunnel.go b/internal/agent/tunnel.go index fb3a44b..80f5778 100644 --- a/internal/agent/tunnel.go +++ b/internal/agent/tunnel.go @@ -26,9 +26,7 @@ func (a *Agent) runTunnel(ctx context.Context, addr string) { case <-ctx.Done(): return } - if backoff < 15*time.Second { - backoff *= 2 - } + backoff = min(backoff*2, retryCap) continue } backoff = time.Second @@ -96,8 +94,14 @@ func (t *tunClient) write(f tunnel.Frame) error { return tunnel.WriteFrame(t.conn, f) } +// tunnelSilence is how long the tunnel may say nothing before it is taken +// for dead: the server pings every 20 s, so this is three missed pings - a +// connection a NAT forgot while the machine slept, say. +const tunnelSilence = 65 * time.Second + func (t *tunClient) readLoop() error { for { + _ = t.conn.SetReadDeadline(time.Now().Add(tunnelSilence)) f, err := tunnel.ReadFrame(t.conn) if err != nil { t.shutdown() @@ -207,9 +211,7 @@ func (a *Agent) runUDP(ctx context.Context, addr string) { case <-ctx.Done(): return } - if backoff < 15*time.Second { - backoff *= 2 - } + backoff = min(backoff*2, retryCap) continue } backoff = time.Second