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
+83
-15
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
texttemplate "text/template"
|
||||
)
|
||||
|
||||
@@ -24,7 +26,8 @@ type Options struct {
|
||||
// nginx TLS); set false only for plain-HTTP local testing.
|
||||
SecureCookies bool
|
||||
// TunnelAddr is the listen address for the direct TCP+UDP forward tunnel
|
||||
// (default ":8446"). Empty disables direct forwarding.
|
||||
// (default ":8446"). Empty disables direct forwarding. The port can be
|
||||
// overridden from the settings page.
|
||||
TunnelAddr string
|
||||
}
|
||||
|
||||
@@ -33,12 +36,22 @@ type Server struct {
|
||||
db *DB
|
||||
hub *Hub
|
||||
fwd *forwardManager
|
||||
tun *tunnelHub
|
||||
udp *udpRelay
|
||||
ufw *ufwManager
|
||||
tmpl *template.Template
|
||||
limiter *loginLimiter
|
||||
distDir string
|
||||
secureCookies bool
|
||||
installTmpl *texttemplate.Template
|
||||
|
||||
// Direct tunnel: bind host and port of the -tunnel address, and the port
|
||||
// currently listened on (the settings override, else the default).
|
||||
// tunnelDefault 0 means the tunnel is disabled.
|
||||
tunnelBind string
|
||||
tunnelDefault int
|
||||
tunnelMu sync.Mutex // serializes port changes
|
||||
tunnelPort atomic.Int64
|
||||
}
|
||||
|
||||
// New builds the server, parsing embedded templates.
|
||||
@@ -65,26 +78,81 @@ func New(opts Options) (*Server, error) {
|
||||
db: opts.DB,
|
||||
hub: hub,
|
||||
fwd: newForwardManager(hub, tun, udp),
|
||||
ufw: newUfwManager(opts.DB, portFromAddr(opts.TunnelAddr)),
|
||||
tun: tun,
|
||||
udp: udp,
|
||||
tmpl: tmpl,
|
||||
limiter: newLoginLimiter(),
|
||||
distDir: opts.DistDir,
|
||||
secureCookies: opts.SecureCookies,
|
||||
installTmpl: installTmpl,
|
||||
}
|
||||
// Start the direct forward tunnel (TCP mux + UDP) on 8446.
|
||||
s.ufw = newUfwManager(opts.DB, s.TunnelPort)
|
||||
// Start the direct forward tunnel (TCP mux + UDP), on the port from the
|
||||
// settings page if one is set there.
|
||||
if opts.TunnelAddr != "" {
|
||||
if err := tun.listen(opts.TunnelAddr); err != nil {
|
||||
log.Printf("tunnel tcp listen %s: %v", opts.TunnelAddr, err)
|
||||
s.tunnelBind, s.tunnelDefault = splitAddr(opts.TunnelAddr)
|
||||
}
|
||||
if s.tunnelDefault > 0 {
|
||||
port := s.tunnelDefault
|
||||
if p := s.db.TunnelPort(); p > 0 {
|
||||
port = p
|
||||
}
|
||||
if err := udp.listen(opts.TunnelAddr); err != nil {
|
||||
log.Printf("tunnel udp listen %s: %v", opts.TunnelAddr, err)
|
||||
s.tunnelPort.Store(int64(port))
|
||||
addr := s.tunnelListenAddr(port)
|
||||
if err := tun.listen(addr); err != nil {
|
||||
log.Printf("tunnel tcp listen %s: %v", addr, err)
|
||||
}
|
||||
if err := udp.listen(addr); err != nil {
|
||||
log.Printf("tunnel udp listen %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
s.restoreForwards()
|
||||
go s.ufw.sync()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// TunnelPort is the port the direct tunnel listens on (0 = disabled).
|
||||
func (s *Server) TunnelPort() int { return int(s.tunnelPort.Load()) }
|
||||
|
||||
func (s *Server) tunnelListenAddr(port int) string {
|
||||
return net.JoinHostPort(s.tunnelBind, strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// tunnelAdvert is the tunnel address handed to agents as "host:port". The host
|
||||
// is empty unless one is set in Settings; agents then use the panel's host.
|
||||
func (s *Server) tunnelAdvert() string {
|
||||
port := s.TunnelPort()
|
||||
if port == 0 {
|
||||
return ""
|
||||
}
|
||||
return net.JoinHostPort(s.db.TunnelHost(), strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// setTunnelPort moves the direct tunnel to another port. Connected agents are
|
||||
// dropped and redial the new port once their next poll tells them about it.
|
||||
func (s *Server) setTunnelPort(port int) error {
|
||||
s.tunnelMu.Lock()
|
||||
defer s.tunnelMu.Unlock()
|
||||
old := s.TunnelPort()
|
||||
if old == 0 || port == old {
|
||||
return nil
|
||||
}
|
||||
addr := s.tunnelListenAddr(port)
|
||||
if err := s.tun.listen(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.udp.listen(addr); err != nil {
|
||||
// Keep TCP and UDP on the same port: go back to the old one.
|
||||
if rerr := s.tun.listen(s.tunnelListenAddr(old)); rerr != nil {
|
||||
log.Printf("tunnel tcp listen %s: %v", s.tunnelListenAddr(old), rerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.tunnelPort.Store(int64(port))
|
||||
go s.ufw.sync()
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreForwards re-opens the host listeners for all saved forward rules.
|
||||
func (s *Server) restoreForwards() {
|
||||
rules, err := s.db.ListForwardRules()
|
||||
@@ -134,6 +202,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("POST /agents/{id}/delete", s.requireAdmin(s.handleAgentDelete))
|
||||
mux.Handle("GET /settings", s.requireAdmin(s.handleSettings))
|
||||
mux.Handle("POST /settings/host", s.requireAdmin(s.handleSetHost))
|
||||
mux.Handle("POST /settings/tunnel", s.requireAdmin(s.handleSetTunnel))
|
||||
mux.Handle("POST /settings/secret/rotate", s.requireAdmin(s.handleRotateSecret))
|
||||
mux.Handle("POST /settings/admins/add", s.requireAdmin(s.handleAddAdmin))
|
||||
mux.Handle("POST /settings/admins/delete", s.requireAdmin(s.handleDeleteAdmin))
|
||||
@@ -157,6 +226,7 @@ func (s *Server) Handler() http.Handler {
|
||||
// Agent API (token/secret auth inside handlers).
|
||||
mux.HandleFunc("POST /api/agent/enroll", s.handleEnroll)
|
||||
mux.HandleFunc("GET /api/agent/poll", s.handlePoll)
|
||||
mux.HandleFunc("GET /api/agent/config", s.handleAgentConfig)
|
||||
mux.HandleFunc("GET /api/agent/term/{sid}/in", s.handleTermIn)
|
||||
mux.HandleFunc("POST /api/agent/term/{sid}/out", s.handleTermOut)
|
||||
mux.HandleFunc("GET /api/agent/task/{id}", s.handleTaskFetch)
|
||||
@@ -202,17 +272,15 @@ func (s *Server) render(w http.ResponseWriter, name string, data map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
// portFromAddr extracts the numeric port from a "host:port" or ":port" address.
|
||||
func portFromAddr(addr string) int {
|
||||
if addr == "" {
|
||||
return 0
|
||||
}
|
||||
_, p, err := net.SplitHostPort(addr)
|
||||
// splitAddr splits a "host:port" or ":port" listen address. The port is 0 when
|
||||
// the address has none.
|
||||
func splitAddr(addr string) (host string, port int) {
|
||||
h, p, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return 0
|
||||
return "", 0
|
||||
}
|
||||
n, _ := strconv.Atoi(p)
|
||||
return n
|
||||
return h, n
|
||||
}
|
||||
|
||||
func clientIP(r *http.Request) string {
|
||||
|
||||
Reference in new issue
Block a user