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.
131 lines
3.9 KiB
Go
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)
|
|
}
|