feat: agents connect by WebSocket, falling back to long polling and trying again each minute; auto, ws or lp per machine (0.1.0)
This commit is contained in:
1 parent
6dd1120a41
commit
73bb720ec6
15 files changed
+676
-22
No files matched your search
@@ -18,6 +18,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
lpra "github.com/x-grk/longpolling-remote-access"
|
||||
@@ -53,6 +54,8 @@ type Agent struct {
|
||||
pollCount int
|
||||
|
||||
// Direct tunnel currently running (see setTunnelAddr).
|
||||
transport atomic.Value // string: auto, ws or lp, as the server says
|
||||
link atomic.Pointer[wsLink]
|
||||
tunMu sync.Mutex
|
||||
tunAddr string
|
||||
tunCancel context.CancelFunc
|
||||
@@ -107,10 +110,22 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
a.setTunnelAddr(ctx, a.cfg.TunnelAddr)
|
||||
}
|
||||
|
||||
// The control channel: a WebSocket, or long polls. In "auto" a failed or
|
||||
// dropped WebSocket gives way to long polling for a minute, then is tried
|
||||
// again; "ws" keeps trying the WebSocket; "lp" only polls.
|
||||
backoff := time.Second
|
||||
for ctx.Err() == nil {
|
||||
if err := a.pollOnce(ctx); err != nil {
|
||||
log.Printf("poll: %v (retry in %s)", err, backoff)
|
||||
if a.mode() == proto.TransportLP {
|
||||
a.longPoll(ctx, 0)
|
||||
continue
|
||||
}
|
||||
err := a.runWS(ctx)
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
switch a.mode() {
|
||||
case proto.TransportWS:
|
||||
log.Printf("websocket: %v (retry in %s)", err, backoff)
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-ctx.Done():
|
||||
@@ -120,10 +135,71 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
case proto.TransportAuto:
|
||||
log.Printf("websocket: %v - long polling, websocket again in a minute", err)
|
||||
}
|
||||
backoff = time.Second
|
||||
a.longPoll(ctx, time.Minute)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mode is the transport the server set for this agent.
|
||||
func (a *Agent) mode() string {
|
||||
if v, ok := a.transport.Load().(string); ok {
|
||||
return v
|
||||
}
|
||||
return proto.TransportAuto
|
||||
}
|
||||
|
||||
func (a *Agent) applyTransport(v string) {
|
||||
switch v {
|
||||
case proto.TransportAuto, proto.TransportWS, proto.TransportLP:
|
||||
if v != a.mode() {
|
||||
log.Printf("transport: %s", v)
|
||||
a.transport.Store(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// longPoll runs control polls: for d, or with d zero until the transport is
|
||||
// no longer long polling.
|
||||
func (a *Agent) longPoll(ctx context.Context, d time.Duration) {
|
||||
until := time.Now().Add(d)
|
||||
backoff := time.Second
|
||||
for ctx.Err() == nil {
|
||||
if d > 0 && (time.Now().After(until) || a.mode() == proto.TransportWS) {
|
||||
return
|
||||
}
|
||||
if d == 0 && a.mode() != proto.TransportLP {
|
||||
return
|
||||
}
|
||||
// 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() {})
|
||||
if d > 0 {
|
||||
reqCtx, cancel = context.WithDeadline(ctx, until)
|
||||
}
|
||||
err := a.pollOnce(ctx, reqCtx)
|
||||
cut := reqCtx.Err() == context.DeadlineExceeded
|
||||
cancel()
|
||||
if err != nil && cut && ctx.Err() == nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("poll: %v (retry in %s)", err, backoff)
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
if backoff < 15*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
backoff = time.Second
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) enroll(ctx context.Context) error {
|
||||
@@ -157,8 +233,10 @@ func (a *Agent) enroll(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// pollOnce performs one control long-poll and dispatches any instruction.
|
||||
func (a *Agent) pollOnce(ctx context.Context) error {
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.Host+"/api/agent/poll", nil)
|
||||
// The poll itself runs on reqCtx, which may end early; what it starts runs on
|
||||
// ctx.
|
||||
func (a *Agent) pollOnce(ctx, reqCtx context.Context) error {
|
||||
req, _ := http.NewRequestWithContext(reqCtx, http.MethodGet, a.cfg.Host+"/api/agent/poll", nil)
|
||||
a.authHeaders(req)
|
||||
// Report the version and our OS user occasionally (not every poll), so the
|
||||
// panel can flag outdated agents without extra traffic.
|
||||
@@ -183,7 +261,14 @@ func (a *Agent) pollOnce(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
a.applyTunnelHeader(ctx, resp.Header)
|
||||
// Dispatch in the background so we keep polling for more work.
|
||||
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
||||
a.dispatch(ctx, msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatch starts what a control message asks for, in the background, so the
|
||||
// control channel keeps listening.
|
||||
func (a *Agent) dispatch(ctx context.Context, msg proto.ControlMessage) {
|
||||
switch msg.Type {
|
||||
case proto.CtrlOpenTerminal:
|
||||
go a.runTerminal(ctx, msg.SessionID, msg.User)
|
||||
@@ -192,7 +277,6 @@ func (a *Agent) pollOnce(ctx context.Context) error {
|
||||
case proto.CtrlTask:
|
||||
go a.runTask(ctx, msg.TaskID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchConfig asks the server for the settings it hands to agents and reports
|
||||
@@ -207,13 +291,17 @@ func (a *Agent) fetchConfig(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
||||
return a.applyTunnelHeader(ctx, resp.Header)
|
||||
}
|
||||
|
||||
// applyTunnelHeader follows the tunnel address the server advertises as
|
||||
// "host:port"; an empty host means the host we already reach the server on.
|
||||
func (a *Agent) applyTunnelHeader(ctx context.Context, h http.Header) bool {
|
||||
v := h.Get("X-Tunnel-Addr")
|
||||
return a.applyTunnelAddr(ctx, h.Get("X-Tunnel-Addr"))
|
||||
}
|
||||
|
||||
func (a *Agent) applyTunnelAddr(ctx context.Context, v string) bool {
|
||||
if v == "" || a.cfg.TunnelPinned {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user