feat: installer fetched from the direct address sets it as the agent's own with the main one as fallback (and downloads from it); agents fall back to whichever address is not theirs; (direct) beside the connection status
This commit is contained in:
1 parent
021da93229
commit
79828e3f8a
11 files changed
+72
-25
No files matched your search
@@ -83,8 +83,10 @@
|
|||||||
DNS-запись без проксирования. Тогда:
|
DNS-запись без проксирования. Тогда:
|
||||||
- агенты, не достучавшись до основного адреса 30 секунд, переходят на прямой и каждые 5 минут проверяют,
|
- агенты, не достучавшись до основного адреса 30 секунд, переходят на прямой и каждые 5 минут проверяют,
|
||||||
не ожил ли основной; уже установленные агенты получают прямой адрес с сервера сами (он хранится в `state.json`);
|
не ожил ли основной; уже установленные агенты получают прямой адрес с сервера сами (он хранится в `state.json`);
|
||||||
- установщик (`FALLBACK_HOST`) качает агента с прямого, если основной не отвечает; в «Полных вариантах»
|
- установщик берёт адреса с того домена, с которого его скачали: с основного - агент работает через основной
|
||||||
есть короткая команда и через прямой адрес;
|
с резервом на прямой, с прямого - наоборот (и бинарник качается с прямого); если первый адрес не отвечает,
|
||||||
|
бинарник качается со второго. В «Полных вариантах» есть короткая команда и через прямой адрес;
|
||||||
|
- в «Изменить → Связь с сервером» у клиента, подключённого через прямой адрес, после статуса стоит «(direct)»;
|
||||||
- если хост туннеля не задан, агентам отдаётся хост прямого адреса;
|
- если хост туннеля не задан, агентам отдаётся хост прямого адреса;
|
||||||
- ключи доступа основного домена работают и на его поддомене.
|
- ключи доступа основного домена работают и на его поддомене.
|
||||||
|
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ func (a *Agent) pollOnce(ctx, reqCtx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
a.applyTunnelHeader(ctx, resp.Header)
|
a.applyTunnelHeader(ctx, resp.Header)
|
||||||
a.learnFallback(resp.Header.Get(proto.FallbackHeader))
|
a.learnAddrs(resp.Header.Get(proto.MainHeader), resp.Header.Get(proto.FallbackHeader))
|
||||||
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
||||||
a.dispatch(ctx, msg)
|
a.dispatch(ctx, msg)
|
||||||
return nil
|
return nil
|
||||||
@@ -355,7 +355,7 @@ func (a *Agent) fetchConfig(ctx context.Context) bool {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
||||||
a.learnFallback(resp.Header.Get(proto.FallbackHeader))
|
a.learnAddrs(resp.Header.Get(proto.MainHeader), resp.Header.Get(proto.FallbackHeader))
|
||||||
return a.applyTunnelHeader(ctx, resp.Header)
|
return a.applyTunnelHeader(ctx, resp.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -54,10 +54,16 @@ func (a *Agent) fallback() string {
|
|||||||
return a.state.Fallback
|
return a.state.Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// learnFallback keeps the direct address the server tells.
|
// learnAddrs keeps the server's other address from the two it tells: the
|
||||||
func (a *Agent) learnFallback(v string) {
|
// direct one, or the main one when the agent was set to the direct one.
|
||||||
v = strings.TrimRight(strings.TrimSpace(v), "/")
|
func (a *Agent) learnAddrs(main, direct string) {
|
||||||
if v == "" || a.cfg.Fallback != "" {
|
main = strings.TrimRight(strings.TrimSpace(main), "/")
|
||||||
|
direct = strings.TrimRight(strings.TrimSpace(direct), "/")
|
||||||
|
v := direct
|
||||||
|
if direct != "" && strings.EqualFold(direct, a.cfg.Host) {
|
||||||
|
v = main
|
||||||
|
}
|
||||||
|
if v == "" || strings.EqualFold(v, a.cfg.Host) || a.cfg.Fallback != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.stateMu.Lock()
|
a.stateMu.Lock()
|
||||||
@@ -67,7 +73,7 @@ func (a *Agent) learnFallback(v string) {
|
|||||||
}
|
}
|
||||||
a.stateMu.Unlock()
|
a.stateMu.Unlock()
|
||||||
if !same {
|
if !same {
|
||||||
log.Printf("direct address: %s", v)
|
log.Printf("fallback address: %s", v)
|
||||||
_ = a.saveState()
|
_ = a.saveState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +169,7 @@ func (a *Agent) reachableAt(ctx context.Context, base string) bool {
|
|||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if resp.StatusCode < 300 {
|
if resp.StatusCode < 300 {
|
||||||
a.learnFallback(resp.Header.Get(proto.FallbackHeader))
|
a.learnAddrs(resp.Header.Get(proto.MainHeader), resp.Header.Get(proto.FallbackHeader))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func (a *Agent) runWS(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
case "cfg":
|
case "cfg":
|
||||||
a.applyTunnelAddr(ctx, m.Tunnel)
|
a.applyTunnelAddr(ctx, m.Tunnel)
|
||||||
a.learnFallback(m.Fallback)
|
a.learnAddrs(m.Main, m.Fallback)
|
||||||
a.applyTransport(m.Transport)
|
a.applyTransport(m.Transport)
|
||||||
if a.mode() == proto.TransportLP {
|
if a.mode() == proto.TransportLP {
|
||||||
return errors.New("set to long polling")
|
return errors.New("set to long polling")
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ const TransportHeader = "X-Transport"
|
|||||||
// to when the main one is out of reach.
|
// to when the main one is out of reach.
|
||||||
const FallbackHeader = "X-Fallback-Host"
|
const FallbackHeader = "X-Fallback-Host"
|
||||||
|
|
||||||
|
// MainHeader carries the server's main address beside FallbackHeader, for an
|
||||||
|
// agent set to the direct one to fall back to the main one.
|
||||||
|
const MainHeader = "X-Main-Host"
|
||||||
|
|
||||||
// WSMsg is one message on an agent's WebSocket, either way. Server to agent:
|
// WSMsg is one message on an agent's WebSocket, either way. Server to agent:
|
||||||
// "ctrl" (Ctrl, as a control poll would answer), "cfg" (Tunnel and
|
// "ctrl" (Ctrl, as a control poll would answer), "cfg" (Tunnel and
|
||||||
// Transport, also a keep-alive), "in" (Frames for session SID), "gone"
|
// Transport, also a keep-alive), "in" (Frames for session SID), "gone"
|
||||||
@@ -91,6 +95,7 @@ type WSMsg struct {
|
|||||||
Tunnel string `json:"tunnel,omitempty"`
|
Tunnel string `json:"tunnel,omitempty"`
|
||||||
Transport string `json:"transport,omitempty"`
|
Transport string `json:"transport,omitempty"`
|
||||||
Fallback string `json:"fallback,omitempty"`
|
Fallback string `json:"fallback,omitempty"`
|
||||||
|
Main string `json:"main,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal / stream frame types on the session "in" channel (server -> agent).
|
// Terminal / stream frame types on the session "in" channel (server -> agent).
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ func (s *Server) handlePoll(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// The agent sends its version and OS user occasionally; record them when
|
// The agent sends its version and OS user occasionally; record them when
|
||||||
// present. Agents older than 0.0.2 send no user, so keep what we have.
|
// present. Agents older than 0.0.2 send no user, so keep what we have.
|
||||||
|
s.viaDirectA.Store(a.ID, s.viaDirect(r))
|
||||||
v := r.Header.Get("X-Agent-Version")
|
v := r.Header.Get("X-Agent-Version")
|
||||||
u := firstNonEmpty(r.Header.Get("X-Agent-User"), a.RunUser)
|
u := firstNonEmpty(r.Header.Get("X-Agent-User"), a.RunUser)
|
||||||
if v != "" && (v != a.Version || u != a.RunUser) {
|
if v != "" && (v != a.Version || u != a.RunUser) {
|
||||||
@@ -69,6 +70,7 @@ func (s *Server) setTunnelHeader(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
if d := s.db.DirectAddress(); d != "" {
|
if d := s.db.DirectAddress(); d != "" {
|
||||||
w.Header().Set(proto.FallbackHeader, d)
|
w.Header().Set(proto.FallbackHeader, d)
|
||||||
|
w.Header().Set(proto.MainHeader, s.db.HostAddress())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
||||||
@@ -227,6 +228,7 @@ func (s *Server) handleAgentEdit(w http.ResponseWriter, r *http.Request) {
|
|||||||
"UserErr": r.URL.Query().Get("uerr"),
|
"UserErr": r.URL.Query().Get("uerr"),
|
||||||
"Transport": firstNonEmpty(a.Transport, proto.TransportAuto),
|
"Transport": firstNonEmpty(a.Transport, proto.TransportAuto),
|
||||||
"OnWS": s.wsAgents.On(a.ID),
|
"OnWS": s.wsAgents.On(a.ID),
|
||||||
|
"ViaDirect": viaDirectOf(&s.viaDirectA, a.ID),
|
||||||
"Online": s.online(a),
|
"Online": s.online(a),
|
||||||
}
|
}
|
||||||
// The user list lives on the machine, so it is only known while it is online.
|
// The user list lives on the machine, so it is only known while it is online.
|
||||||
@@ -437,7 +439,7 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Host": host,
|
"Host": host,
|
||||||
"Direct": direct,
|
"Direct": direct,
|
||||||
"DirectShort": fmt.Sprintf("wget -qO- %s/i/%s | sudo sh", direct, code),
|
"DirectShort": fmt.Sprintf("wget -qO- %s/i/%s | sudo sh", direct, code),
|
||||||
"DirectCurl": fmt.Sprintf("curl -fsSL %s/install.sh | sudo HOST=%s FALLBACK_HOST=%s ENROLL_SECRET=%s sh", direct, host, direct, secret),
|
"DirectCurl": fmt.Sprintf("curl -fsSL %s/install.sh | sudo HOST=%s FALLBACK_HOST=%s ENROLL_SECRET=%s sh", direct, direct, host, secret),
|
||||||
"Secret": secret,
|
"Secret": secret,
|
||||||
"CodeLen": s.db.InstallCodeLen(),
|
"CodeLen": s.db.InstallCodeLen(),
|
||||||
"Admins": admins,
|
"Admins": admins,
|
||||||
@@ -664,7 +666,7 @@ func (s *Server) handleUfwRevoke(w http.ResponseWriter, r *http.Request) {
|
|||||||
// handleInstallScript serves the generic installer; the secret is NOT baked in,
|
// handleInstallScript serves the generic installer; the secret is NOT baked in,
|
||||||
// so it must be supplied via env or the interactive prompt.
|
// so it must be supplied via env or the interactive prompt.
|
||||||
func (s *Server) handleInstallScript(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleInstallScript(w http.ResponseWriter, r *http.Request) {
|
||||||
s.renderInstall(w, "")
|
s.renderInstall(w, r, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleShortInstall serves the installer behind the short /i/<code> link with
|
// handleShortInstall serves the installer behind the short /i/<code> link with
|
||||||
@@ -686,16 +688,35 @@ func (s *Server) handleShortInstall(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.renderInstall(w, s.db.EnrollSecret())
|
s.renderInstall(w, r, s.db.EnrollSecret())
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderInstall writes install.sh with the current host and an optional secret.
|
// viaDirect reports whether the request came to the direct address.
|
||||||
func (s *Server) renderInstall(w http.ResponseWriter, secret string) {
|
func (s *Server) viaDirect(r *http.Request) bool {
|
||||||
|
d := directHost(s.db.DirectAddress())
|
||||||
|
if d == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host := r.Host
|
||||||
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
return strings.EqualFold(host, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderInstall writes install.sh with the server's addresses and an
|
||||||
|
// optional secret. Fetched from the direct address, it makes that the
|
||||||
|
// agent's address and the main one its fallback; otherwise the other way.
|
||||||
|
func (s *Server) renderInstall(w http.ResponseWriter, r *http.Request, secret string) {
|
||||||
|
host, fallback := s.db.HostAddress(), s.db.DirectAddress()
|
||||||
|
if s.viaDirect(r) {
|
||||||
|
host, fallback = fallback, host
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/x-shellscript; charset=utf-8")
|
w.Header().Set("Content-Type", "text/x-shellscript; charset=utf-8")
|
||||||
if err := s.installTmpl.Execute(w, map[string]any{
|
if err := s.installTmpl.Execute(w, map[string]any{
|
||||||
"Host": s.db.HostAddress(),
|
"Host": host,
|
||||||
"Direct": s.db.DirectAddress(),
|
"Fallback": fallback,
|
||||||
"Secret": secret,
|
"Secret": secret,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("install.sh render: %v", err)
|
log.Printf("install.sh render: %v", err)
|
||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
@@ -707,3 +728,11 @@ func (s *Server) downloadHandler() http.Handler {
|
|||||||
// install one-liner works before the machine is enrolled.
|
// install one-liner works before the machine is enrolled.
|
||||||
return http.FileServer(http.Dir(s.distDir))
|
return http.FileServer(http.Dir(s.distDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viaDirectOf tells whether the agent last reached the server at the direct
|
||||||
|
// address.
|
||||||
|
func viaDirectOf(m *sync.Map, id string) bool {
|
||||||
|
v, _ := m.Load(id)
|
||||||
|
b, _ := v.(bool)
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ type Server struct {
|
|||||||
meters speedMeters // byte counts of the speed tests under way
|
meters speedMeters // byte counts of the speed tests under way
|
||||||
xfers xfers // files on their way between a browser and an agent
|
xfers xfers // files on their way between a browser and an agent
|
||||||
ship shipping // files on their way from one machine to another
|
ship shipping // files on their way from one machine to another
|
||||||
|
viaDirectA sync.Map // agent id -> true while it reaches the server at the direct address
|
||||||
challenges challenges // passkey challenges handed out
|
challenges challenges // passkey challenges handed out
|
||||||
wsAgents wsAgents // agents on their WebSocket now
|
wsAgents wsAgents // agents on their WebSocket now
|
||||||
pinMisses pinMisses // wrong PINs per session
|
pinMisses pinMisses // wrong PINs per session
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
HOST="${HOST:-{{.Host}}}"
|
HOST="${HOST:-{{.Host}}}"
|
||||||
# The direct address (not behind a proxy like Cloudflare): the agent turns to
|
# The server's other address (the direct one, not behind a proxy like
|
||||||
# it when the main one is out of reach.
|
# Cloudflare, or the main one when this script came from the direct one): the
|
||||||
FALLBACK_HOST="${FALLBACK_HOST:-{{.Direct}}}"
|
# agent turns to it when HOST is out of reach.
|
||||||
|
FALLBACK_HOST="${FALLBACK_HOST:-{{.Fallback}}}"
|
||||||
{{if .Secret}}ENROLL_SECRET="${ENROLL_SECRET:-{{.Secret}}}"
|
{{if .Secret}}ENROLL_SECRET="${ENROLL_SECRET:-{{.Secret}}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
BIN_PATH="/usr/local/bin/lpagent"
|
BIN_PATH="/usr/local/bin/lpagent"
|
||||||
@@ -123,7 +124,7 @@ bold "Скачивание агента ($ARCH) с $HOST…"
|
|||||||
TMP="$(mktemp)"
|
TMP="$(mktemp)"
|
||||||
if ! download "$HOST/download/agent-linux-$ARCH" "$TMP"; then
|
if ! download "$HOST/download/agent-linux-$ARCH" "$TMP"; then
|
||||||
if [ -n "$FALLBACK_HOST" ] && [ "$FALLBACK_HOST" != "$HOST" ]; then
|
if [ -n "$FALLBACK_HOST" ] && [ "$FALLBACK_HOST" != "$HOST" ]; then
|
||||||
bold "Основной адрес недоступен, скачивание с $FALLBACK_HOST…"
|
bold "$HOST недоступен, скачивание с $FALLBACK_HOST…"
|
||||||
download "$FALLBACK_HOST/download/agent-linux-$ARCH" "$TMP"
|
download "$FALLBACK_HOST/download/agent-linux-$ARCH" "$TMP"
|
||||||
else
|
else
|
||||||
red "Не удалось скачать агента."; exit 1
|
red "Не удалось скачать агента."; exit 1
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Связь с сервером</h2>
|
<h2>Связь с сервером</h2>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Сейчас: {{if not .Online}}<span>офлайн</span>{{else if .OnWS}}<span style="color:var(--ok)">WebSocket</span>{{else}}<span>long polling</span>{{end}}
|
Сейчас: {{if not .Online}}<span>офлайн</span>{{else if .OnWS}}<span style="color:var(--ok)">WebSocket</span>{{else}}<span>long polling</span>{{end}}{{if and .Online .ViaDirect}} <span class="muted">(direct)</span>{{end}}
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action="/agents/{{.Agent.ID}}/transport" class="row">
|
<form method="post" action="/agents/{{.Agent.ID}}/transport" class="row">
|
||||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func (s *Server) handleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.viaDirectA.Store(a.ID, s.viaDirect(r))
|
||||||
conn.SetReadLimit(4 << 20)
|
conn.SetReadLimit(4 << 20)
|
||||||
ctx, cancel := context.WithCancel(r.Context())
|
ctx, cancel := context.WithCancel(r.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -97,7 +98,7 @@ func (s *Server) handleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
return wsjson.Write(wctx, conn, m)
|
return wsjson.Write(wctx, conn, m)
|
||||||
}
|
}
|
||||||
cfg := func() proto.WSMsg {
|
cfg := func() proto.WSMsg {
|
||||||
return proto.WSMsg{T: "cfg", Tunnel: s.tunnelAdvert(), Transport: s.transportOf(a.ID), Fallback: s.db.DirectAddress()}
|
return proto.WSMsg{T: "cfg", Tunnel: s.tunnelAdvert(), Transport: s.transportOf(a.ID), Fallback: s.db.DirectAddress(), Main: s.db.HostAddress()}
|
||||||
}
|
}
|
||||||
if send(cfg()) != nil {
|
if send(cfg()) != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in new issue
Block a user