Files
lpra/internal/server/handlers_panel_fwd.go
T
redtea f3cb7701f2 fix: remove an agent's forward rules with the agent (0.0.4)
Deleting a machine left its forward rules behind: the host ports stayed
bound and "busy" with no page to remove them from. The rules, their
listeners and ufw rules now go with the agent, and rules orphaned by
older versions are dropped on start.

A host port equal to the direct tunnel port gets its own message.
2026-10-13 16:30:53 +03:00

131 lines
3.9 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": "Этот порт хоста уже занят другим правилом.",
"tunnel": "Этот порт занят прямым туннелем панели (см. Настройки).",
"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 hostPort == s.TunnelPort() {
http.Redirect(w, r, "/agents/"+id+"/forwards?err=tunnel", http.StatusSeeOther)
return
}
if s.db.HostPortUsed(hostPort) {
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.removeForward(rid)
dest := "/"
if rule != nil {
dest = "/agents/" + rule.AgentID + "/forwards"
}
http.Redirect(w, r, dest, http.StatusSeeOther)
}
// removeForward closes a forward rule's listener and deletes the rule, along
// with the ufw rule bound to it (the shared tunnel port stays while another
// direct forward needs it).
func (s *Server) removeForward(rid int64) {
s.fwd.close(rid)
_ = s.db.DeleteForwardRule(rid)
s.ufw.revokeForRule(rid)
}