feat: tunnel host and port in the panel settings, ufw kept in sync
The direct tunnel port (8446 by default) and the host agents dial for it can be set in Settings. The server moves its listeners at once and agents pick the address up from the panel: on connect and with every poll, unless TUNNEL_ADDR / TUNNEL_PORT pin it locally. ufw: run with a C locale (the output is translated), do not adopt rules that existed before the panel, skip loopback-only listeners, re-open what saved forwards need on start and when the integration is turned on, move the shared tunnel rule with the port and drop it when no direct forward is left. The last ufw error is shown in Settings. The forwards page shows the real tunnel and panel ports.
This commit is contained in:
1 parent
08fc2d79a8
commit
bcfe909aaa
15 files changed
+540
-97
No files matched your search
@@ -13,9 +13,11 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
lpra "github.com/x-grk/longpolling-remote-access"
|
||||
@@ -32,6 +34,9 @@ type Config struct {
|
||||
Name string // optional friendly name
|
||||
StatePath string // where the enrolled id+token are persisted
|
||||
TunnelAddr string // host:port for the direct forward tunnel (e.g. REDACTED:8446); empty disables
|
||||
// TunnelPinned keeps TunnelAddr as configured locally (TUNNEL_ADDR /
|
||||
// TUNNEL_PORT) instead of following the address set in the panel.
|
||||
TunnelPinned bool
|
||||
}
|
||||
|
||||
type state struct {
|
||||
@@ -45,6 +50,11 @@ type Agent struct {
|
||||
http *http.Client
|
||||
state state
|
||||
pollCount int
|
||||
|
||||
// Direct tunnel currently running (see setTunnelAddr).
|
||||
tunMu sync.Mutex
|
||||
tunAddr string
|
||||
tunCancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New builds an agent. It does not perform any network I/O.
|
||||
@@ -87,10 +97,11 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
}
|
||||
log.Printf("agent %s connected to %s", a.state.AgentID, a.cfg.Host)
|
||||
|
||||
// Direct forward tunnel (low-latency TCP mux + UDP) on 8446, if configured.
|
||||
if a.cfg.TunnelAddr != "" {
|
||||
go a.runTunnel(ctx)
|
||||
go a.runUDP(ctx)
|
||||
// Direct forward tunnel (low-latency TCP mux + UDP). Its address comes
|
||||
// from the panel; fall back to the configured one when the server sends
|
||||
// none (tunnel port left at the default on an older server).
|
||||
if a.cfg.TunnelPinned || !a.fetchConfig(ctx) {
|
||||
a.setTunnelAddr(ctx, a.cfg.TunnelAddr)
|
||||
}
|
||||
|
||||
backoff := time.Second
|
||||
@@ -168,6 +179,7 @@ func (a *Agent) pollOnce(ctx context.Context) error {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&msg); err != nil {
|
||||
return err
|
||||
}
|
||||
a.applyTunnelHeader(ctx, resp.Header)
|
||||
// Dispatch in the background so we keep polling for more work.
|
||||
switch msg.Type {
|
||||
case proto.CtrlOpenTerminal:
|
||||
@@ -180,6 +192,66 @@ func (a *Agent) pollOnce(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchConfig asks the server for the settings it hands to agents and reports
|
||||
// whether it named a tunnel address.
|
||||
func (a *Agent) fetchConfig(ctx context.Context) bool {
|
||||
ctx2, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequestWithContext(ctx2, http.MethodGet, a.cfg.Host+"/api/agent/config", nil)
|
||||
a.authHeaders(req)
|
||||
resp, err := a.http.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
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")
|
||||
if v == "" || a.cfg.TunnelPinned {
|
||||
return false
|
||||
}
|
||||
host, port, err := net.SplitHostPort(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if host == "" {
|
||||
u, err := url.Parse(a.cfg.Host)
|
||||
if err != nil || u.Hostname() == "" {
|
||||
return false
|
||||
}
|
||||
host = u.Hostname()
|
||||
}
|
||||
a.setTunnelAddr(ctx, net.JoinHostPort(host, port))
|
||||
return true
|
||||
}
|
||||
|
||||
// setTunnelAddr points the direct tunnel at addr, restarting it if it was
|
||||
// running against another address. An empty addr stops it.
|
||||
func (a *Agent) setTunnelAddr(ctx context.Context, addr string) {
|
||||
a.tunMu.Lock()
|
||||
defer a.tunMu.Unlock()
|
||||
if addr == a.tunAddr {
|
||||
return
|
||||
}
|
||||
if a.tunCancel != nil {
|
||||
a.tunCancel()
|
||||
a.tunCancel = nil
|
||||
}
|
||||
a.tunAddr = addr
|
||||
if addr == "" {
|
||||
return
|
||||
}
|
||||
log.Printf("tunnel: using %s", addr)
|
||||
tctx, cancel := context.WithCancel(ctx)
|
||||
a.tunCancel = cancel
|
||||
go a.runTunnel(tctx, addr)
|
||||
go a.runUDP(tctx, addr)
|
||||
}
|
||||
|
||||
func (a *Agent) authHeaders(req *http.Request) {
|
||||
req.Header.Set("X-Agent-Id", a.state.AgentID)
|
||||
req.Header.Set("Authorization", "Bearer "+a.state.Token)
|
||||
|
||||
Reference in new issue
Block a user