Files
lpra/internal/server/handlers_panel_fwd.go
T
redtea bcfe909aaa 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.
2026-10-13 14:50:50 +03:00

121 lines
3.6 KiB
Go

package server
import (
"log"
"net/http"
"net/url"
"strconv"
"strings"
)
func (s *Server) handleForwardsPage(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
a, err := s.db.GetAgent(id)
if err != nil || a == nil {
http.NotFound(w, r)
return
}
rules, _ := s.db.ListForwardRulesByAgent(id)
errMsg := map[string]string{
"busy": "Этот порт хоста уже занят другим правилом.",
"port": "Некорректный номер порта.",
"listen": "Не удалось занять порт на хосте (возможно, уже используется).",
"db": "Ошибка базы данных.",
}[r.URL.Query().Get("err")]
s.render(w, "forwards.html", map[string]any{
"Agent": a,
"Rules": rules,
"CSRF": csrfToken(r),
"Error": errMsg,
"TunnelPort": s.TunnelPort(),
"PanelPort": panelPort(s.db.HostAddress()),
})
}
// panelPort is the port of the panel URL agents dial, for the mode hints.
func panelPort(host string) string {
u, err := url.Parse(host)
if err != nil {
return ""
}
if p := u.Port(); p != "" {
return p
}
if u.Scheme == "http" {
return "80"
}
return "443"
}
// POST /agents/{id}/forwards/add
func (s *Server) handleForwardAdd(w http.ResponseWriter, r *http.Request) {
if !checkCSRF(r) {
http.Error(w, "bad csrf", http.StatusForbidden)
return
}
id := r.PathValue("id")
a, err := s.db.GetAgent(id)
if err != nil || a == nil {
http.NotFound(w, r)
return
}
clientPort, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("client_port")))
hostPort, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("host_port")))
listenAddr := strings.TrimSpace(r.FormValue("listen_addr"))
if listenAddr == "" {
listenAddr = "0.0.0.0"
}
protoKind := strings.TrimSpace(r.FormValue("proto"))
switch protoKind {
case "http", "tcp", "udp":
default:
protoKind = "http"
}
if clientPort < 1 || clientPort > 65535 || hostPort < 1 || hostPort > 65535 {
http.Redirect(w, r, "/agents/"+id+"/forwards?err=port", http.StatusSeeOther)
return
}
// Reject a host port already bound by another forward rule or the tunnel.
if s.db.HostPortUsed(hostPort) || hostPort == s.TunnelPort() {
http.Redirect(w, r, "/agents/"+id+"/forwards?err=busy", http.StatusSeeOther)
return
}
rule := ForwardRule{AgentID: id, ClientPort: clientPort, HostPort: hostPort, ListenAddr: listenAddr, Proto: protoKind}
ruleID, err := s.db.CreateForwardRule(rule)
if err != nil {
http.Redirect(w, r, "/agents/"+id+"/forwards?err=db", http.StatusSeeOther)
return
}
rule.ID = ruleID
if err := s.fwd.open(rule); err != nil {
// Roll back the rule if the port can't be bound.
_ = s.db.DeleteForwardRule(ruleID)
log.Printf("forward open %s:%d: %v", listenAddr, hostPort, err)
http.Redirect(w, r, "/agents/"+id+"/forwards?err=listen", http.StatusSeeOther)
return
}
// Open the port in ufw too (if integration is enabled and ufw is active).
s.ufw.allowForward(rule)
http.Redirect(w, r, "/agents/"+id+"/forwards", http.StatusSeeOther)
}
// POST /forwards/{rid}/delete
func (s *Server) handleForwardDelete(w http.ResponseWriter, r *http.Request) {
if !checkCSRF(r) {
http.Error(w, "bad csrf", http.StatusForbidden)
return
}
rid, _ := strconv.ParseInt(r.PathValue("rid"), 10, 64)
rule, _ := s.db.GetForwardRule(rid)
s.fwd.close(rid)
_ = s.db.DeleteForwardRule(rid)
// Remove the ufw rule bound to this forward rule (the shared tunnel port
// stays while another direct forward needs it).
s.ufw.revokeForRule(rid)
dest := "/"
if rule != nil {
dest = "/agents/" + rule.AgentID + "/forwards"
}
http.Redirect(w, r, dest, http.StatusSeeOther)
}