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) }