feat: open a port in ufw by hand from the panel settings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87921a1381
commit
789e1da93b
5 files changed
+77
-6
No files matched your search
+14
-1
@@ -402,7 +402,20 @@ type UfwRule struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Port int
|
Port int
|
||||||
Proto string
|
Proto string
|
||||||
RuleID int64 // bound forward rule id, or 0 for shared/manual (e.g. the tunnel port)
|
RuleID int64 // bound forward rule id, 0 for shared (the tunnel port), ufwManual for one added in Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// ufwManual is the RuleID of a ufw rule the admin added by hand: no forward
|
||||||
|
// owns it, so only the admin removes it.
|
||||||
|
const ufwManual = -1
|
||||||
|
|
||||||
|
// Manual reports whether the admin added the rule by hand.
|
||||||
|
func (r UfwRule) Manual() bool { return r.RuleID == ufwManual }
|
||||||
|
|
||||||
|
// SetUfwRuleManual hands a tracked rule over to the admin.
|
||||||
|
func (db *DB) SetUfwRuleManual(port int, proto string) error {
|
||||||
|
_, err := db.sql.Exec(`UPDATE ufw_rules SET rule_id = ? WHERE port = ? AND proto = ?`, ufwManual, port, proto)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) AddUfwRule(port int, proto string, ruleID int64) error {
|
func (db *DB) AddUfwRule(port int, proto string, ruleID int64) error {
|
||||||
|
|||||||
@@ -398,6 +398,10 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Error": map[string]string{
|
"Error": map[string]string{
|
||||||
"tunnel_host": "Некорректный хост туннеля: нужен IP или имя без схемы и порта.",
|
"tunnel_host": "Некорректный хост туннеля: нужен IP или имя без схемы и порта.",
|
||||||
"tunnel_port": "Некорректный порт туннеля.",
|
"tunnel_port": "Некорректный порт туннеля.",
|
||||||
|
"ufw_port": "Некорректное правило ufw: нужен порт 1-65535 и протокол tcp или udp.",
|
||||||
|
"ufw_off": "Правило ufw не добавлено: ufw выключен или интеграция с ним отключена.",
|
||||||
|
"ufw_failed": "Правило ufw не добавлено: команда ufw завершилась ошибкой.",
|
||||||
|
"ufw_exists": "Этот порт уже открыт в ufw не панелью - правило осталось как было, панель его не отслеживает.",
|
||||||
"tunnel_listen": "Не удалось занять новый порт туннеля на хосте (возможно, уже используется). Порт не изменён.",
|
"tunnel_listen": "Не удалось занять новый порт туннеля на хосте (возможно, уже используется). Порт не изменён.",
|
||||||
}[r.URL.Query().Get("err")],
|
}[r.URL.Query().Get("err")],
|
||||||
})
|
})
|
||||||
@@ -525,6 +529,24 @@ func (s *Server) handleUfwToggle(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/settings", http.StatusSeeOther)
|
http.Redirect(w, r, "/settings", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUfwAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !checkCSRF(r) {
|
||||||
|
http.Error(w, "bad csrf", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
port, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("port")))
|
||||||
|
proto := r.FormValue("proto")
|
||||||
|
if port < 1 || port > 65535 || (proto != "tcp" && proto != "udp") {
|
||||||
|
http.Redirect(w, r, "/settings?err=ufw_port", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target := "/settings"
|
||||||
|
if key := s.ufw.allowManual(port, proto); key != "" {
|
||||||
|
target += "?err=" + key
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleUfwRevoke(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUfwRevoke(w http.ResponseWriter, r *http.Request) {
|
||||||
if !checkCSRF(r) {
|
if !checkCSRF(r) {
|
||||||
http.Error(w, "bad csrf", http.StatusForbidden)
|
http.Error(w, "bad csrf", http.StatusForbidden)
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.Handle("POST /settings/admins/add", s.requireAdmin(s.handleAddAdmin))
|
mux.Handle("POST /settings/admins/add", s.requireAdmin(s.handleAddAdmin))
|
||||||
mux.Handle("POST /settings/admins/delete", s.requireAdmin(s.handleDeleteAdmin))
|
mux.Handle("POST /settings/admins/delete", s.requireAdmin(s.handleDeleteAdmin))
|
||||||
mux.Handle("POST /settings/ufw/toggle", s.requireAdmin(s.handleUfwToggle))
|
mux.Handle("POST /settings/ufw/toggle", s.requireAdmin(s.handleUfwToggle))
|
||||||
|
mux.Handle("POST /settings/ufw/add", s.requireAdmin(s.handleUfwAdd))
|
||||||
mux.Handle("POST /settings/ufw/revoke", s.requireAdmin(s.handleUfwRevoke))
|
mux.Handle("POST /settings/ufw/revoke", s.requireAdmin(s.handleUfwRevoke))
|
||||||
|
|
||||||
// Panel terminal over HTTP long-polling (admin-only).
|
// Panel terminal over HTTP long-polling (admin-only).
|
||||||
|
|||||||
+25
-5
@@ -87,26 +87,46 @@ func (u *ufwManager) LastError() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// allow opens port/proto in ufw and records it, bound to the given forward
|
// allow opens port/proto in ufw and records it, bound to the given forward
|
||||||
// rule id (0 = shared/manual). The caller has checked available().
|
// rule id (0 = shared, ufwManual = added in Settings). It reports whether the
|
||||||
func (u *ufwManager) allow(port int, proto string, ruleID int64) {
|
// panel now tracks the rule. The caller has checked available().
|
||||||
|
func (u *ufwManager) allow(port int, proto string, ruleID int64) bool {
|
||||||
if u.db.HasUfwRule(port, proto) {
|
if u.db.HasUfwRule(port, proto) {
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
spec := fmt.Sprintf("%d/%s", port, proto)
|
spec := fmt.Sprintf("%d/%s", port, proto)
|
||||||
out, err := ufwRun("allow", spec)
|
out, err := ufwRun("allow", spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ufw allow %s: %v: %s", spec, err, out)
|
log.Printf("ufw allow %s: %v: %s", spec, err, out)
|
||||||
u.setErr(fmt.Sprintf("ufw allow %s: %v: %s", spec, err, out))
|
u.setErr(fmt.Sprintf("ufw allow %s: %v: %s", spec, err, out))
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
u.setErr("")
|
u.setErr("")
|
||||||
if strings.Contains(out, "Skipping") {
|
if strings.Contains(out, "Skipping") {
|
||||||
// The rule was there before the panel asked for it, so it is not ours
|
// The rule was there before the panel asked for it, so it is not ours
|
||||||
// to track (and later delete).
|
// to track (and later delete).
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
_ = u.db.AddUfwRule(port, proto, ruleID)
|
_ = u.db.AddUfwRule(port, proto, ruleID)
|
||||||
log.Printf("ufw: allowed %s", spec)
|
log.Printf("ufw: allowed %s", spec)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowManual opens a port the admin asked for in Settings. A port the panel
|
||||||
|
// already opened for a forward or the tunnel becomes the admin's, so it stays
|
||||||
|
// when they no longer need it. It returns the error key for the settings page
|
||||||
|
// ("" on success).
|
||||||
|
func (u *ufwManager) allowManual(port int, proto string) string {
|
||||||
|
if !u.available() {
|
||||||
|
return "ufw_off"
|
||||||
|
}
|
||||||
|
if !u.allow(port, proto, ufwManual) {
|
||||||
|
if u.LastError() != "" {
|
||||||
|
return "ufw_failed"
|
||||||
|
}
|
||||||
|
return "ufw_exists"
|
||||||
|
}
|
||||||
|
_ = u.db.SetUfwRuleManual(port, proto)
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// revokeRule deletes one tracked ufw rule from ufw and the tracking table.
|
// revokeRule deletes one tracked ufw rule from ufw and the tracking table.
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
{{range .UfwRules}}
|
{{range .UfwRules}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>{{.Port}}/{{.Proto}}</code></td>
|
<td><code>{{.Port}}/{{.Proto}}</code></td>
|
||||||
|
<td class="muted">{{if .Manual}}вручную{{else if .RuleID}}проброс{{else}}туннель{{end}}</td>
|
||||||
<td style="text-align:right">
|
<td style="text-align:right">
|
||||||
<form method="post" action="/settings/ufw/revoke" style="display:inline"
|
<form method="post" action="/settings/ufw/revoke" style="display:inline"
|
||||||
onsubmit="return confirm('Отменить правило ufw {{.Port}}/{{.Proto}}?')">
|
onsubmit="return confirm('Отменить правило ufw {{.Port}}/{{.Proto}}?')">
|
||||||
@@ -156,6 +157,20 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<p class="muted" style="font-size:13px; margin-top:10px">Панель пока не открывала портов в ufw.</p>
|
<p class="muted" style="font-size:13px; margin-top:10px">Панель пока не открывала портов в ufw.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if and .UfwActive .UfwEnabled}}
|
||||||
|
<h2 style="margin-top:18px; font-size:15px">Открыть порт вручную</h2>
|
||||||
|
<form method="post" action="/settings/ufw/add" class="row">
|
||||||
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||||
|
<input type="number" name="port" min="1" max="65535" placeholder="порт" style="max-width:120px" required>
|
||||||
|
<select name="proto" style="max-width:100px">
|
||||||
|
<option value="tcp">tcp</option>
|
||||||
|
<option value="udp">udp</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Открыть</button>
|
||||||
|
</form>
|
||||||
|
<p class="muted" style="font-size:13px">Такое правило не привязано к пробросу и остаётся, пока его не отменить здесь.</p>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user