feat: optional label on a forward rule

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
redteaandClaude Opus 5.5 committed 2026-10-13 20:31:41 +03:00
1 parent 201d63ee1e
commit b51363bf72
3 files changed
+28 -9

No files matched your search

+9 -7
View File
@@ -106,6 +106,7 @@ CREATE TABLE IF NOT EXISTS forward_rules (
`ALTER TABLE agents ADD COLUMN run_user TEXT NOT NULL DEFAULT ''`, `ALTER TABLE agents ADD COLUMN run_user TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE agents ADD COLUMN login_user TEXT NOT NULL DEFAULT ''`, `ALTER TABLE agents ADD COLUMN login_user TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE forward_rules ADD COLUMN no_ufw INTEGER NOT NULL DEFAULT 0`, `ALTER TABLE forward_rules ADD COLUMN no_ufw INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE forward_rules ADD COLUMN label TEXT NOT NULL DEFAULT ''`,
} { } {
if _, err := db.sql.Exec(alter); err != nil && !strings.Contains(err.Error(), "duplicate column") { if _, err := db.sql.Exec(alter); err != nil && !strings.Contains(err.Error(), "duplicate column") {
return err return err
@@ -330,6 +331,7 @@ type ForwardRule struct {
ListenAddr string ListenAddr string
Proto string Proto string
NoUfw bool // the panel opens no ufw rule for this forward NoUfw bool // the panel opens no ufw rule for this forward
Label string // optional note shown next to the rule
} }
func (db *DB) CreateForwardRule(r ForwardRule) (int64, error) { func (db *DB) CreateForwardRule(r ForwardRule) (int64, error) {
@@ -337,9 +339,9 @@ func (db *DB) CreateForwardRule(r ForwardRule) (int64, error) {
r.Proto = "http" r.Proto = "http"
} }
res, err := db.sql.Exec( res, err := db.sql.Exec(
`INSERT INTO forward_rules(agent_id, client_port, host_port, listen_addr, proto, no_ufw, created_at) `INSERT INTO forward_rules(agent_id, client_port, host_port, listen_addr, proto, no_ufw, label, created_at)
VALUES(?, ?, ?, ?, ?, ?, ?)`, VALUES(?, ?, ?, ?, ?, ?, ?, ?)`,
r.AgentID, r.ClientPort, r.HostPort, r.ListenAddr, r.Proto, r.NoUfw, time.Now().Unix()) r.AgentID, r.ClientPort, r.HostPort, r.ListenAddr, r.Proto, r.NoUfw, r.Label, time.Now().Unix())
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -348,7 +350,7 @@ func (db *DB) CreateForwardRule(r ForwardRule) (int64, error) {
func (db *DB) ListForwardRules() ([]ForwardRule, error) { func (db *DB) ListForwardRules() ([]ForwardRule, error) {
rows, err := db.sql.Query( rows, err := db.sql.Query(
`SELECT id, agent_id, client_port, host_port, listen_addr, proto, no_ufw FROM forward_rules ORDER BY id`) `SELECT id, agent_id, client_port, host_port, listen_addr, proto, no_ufw, label FROM forward_rules ORDER BY id`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -356,7 +358,7 @@ func (db *DB) ListForwardRules() ([]ForwardRule, error) {
var out []ForwardRule var out []ForwardRule
for rows.Next() { for rows.Next() {
var r ForwardRule var r ForwardRule
if err := rows.Scan(&r.ID, &r.AgentID, &r.ClientPort, &r.HostPort, &r.ListenAddr, &r.Proto, &r.NoUfw); err != nil { if err := rows.Scan(&r.ID, &r.AgentID, &r.ClientPort, &r.HostPort, &r.ListenAddr, &r.Proto, &r.NoUfw, &r.Label); err != nil {
return nil, err return nil, err
} }
out = append(out, r) out = append(out, r)
@@ -381,8 +383,8 @@ func (db *DB) ListForwardRulesByAgent(agentID string) ([]ForwardRule, error) {
func (db *DB) GetForwardRule(id int64) (*ForwardRule, error) { func (db *DB) GetForwardRule(id int64) (*ForwardRule, error) {
r := &ForwardRule{} r := &ForwardRule{}
err := db.sql.QueryRow( err := db.sql.QueryRow(
`SELECT id, agent_id, client_port, host_port, listen_addr, proto, no_ufw FROM forward_rules WHERE id = ?`, id). `SELECT id, agent_id, client_port, host_port, listen_addr, proto, no_ufw, label FROM forward_rules WHERE id = ?`, id).
Scan(&r.ID, &r.AgentID, &r.ClientPort, &r.HostPort, &r.ListenAddr, &r.Proto, &r.NoUfw) Scan(&r.ID, &r.AgentID, &r.ClientPort, &r.HostPort, &r.ListenAddr, &r.Proto, &r.NoUfw, &r.Label)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} }
+13 -1
View File
@@ -87,7 +87,7 @@ func (s *Server) handleForwardAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
rule := ForwardRule{AgentID: id, ClientPort: clientPort, HostPort: hostPort, ListenAddr: listenAddr, Proto: protoKind, rule := ForwardRule{AgentID: id, ClientPort: clientPort, HostPort: hostPort, ListenAddr: listenAddr, Proto: protoKind,
NoUfw: r.FormValue("no_ufw") != ""} NoUfw: r.FormValue("no_ufw") != "", Label: forwardLabel(r.FormValue("label"))}
ruleID, err := s.db.CreateForwardRule(rule) ruleID, err := s.db.CreateForwardRule(rule)
if err != nil { if err != nil {
http.Redirect(w, r, "/agents/"+id+"/forwards?err=db", http.StatusSeeOther) http.Redirect(w, r, "/agents/"+id+"/forwards?err=db", http.StatusSeeOther)
@@ -130,3 +130,15 @@ func (s *Server) removeForward(rid int64) {
_ = s.db.DeleteForwardRule(rid) _ = s.db.DeleteForwardRule(rid)
s.ufw.revokeForRule(rid) s.ufw.revokeForRule(rid)
} }
// forwardLabelMax bounds a rule's label, in characters.
const forwardLabelMax = 80
// forwardLabel tidies the optional note of a rule: one line, bounded length.
func forwardLabel(v string) string {
v = strings.Join(strings.Fields(v), " ")
if r := []rune(v); len(r) > forwardLabelMax {
v = string(r[:forwardLabelMax])
}
return v
}
+6 -1
View File
@@ -40,7 +40,7 @@
<h2>Правила</h2> <h2>Правила</h2>
{{if .Rules}} {{if .Rules}}
<table> <table>
<thead><tr><th>Тип</th><th>Хост (порт)</th><th>→ Клиент (порт)</th><th>Слушать на</th><th></th></tr></thead> <thead><tr><th>Тип</th><th>Хост (порт)</th><th>→ Клиент (порт)</th><th>Слушать на</th><th>Метка</th><th></th></tr></thead>
<tbody> <tbody>
{{range .Rules}} {{range .Rules}}
<tr> <tr>
@@ -48,6 +48,7 @@
<td class="muted">:{{.HostPort}}</td> <td class="muted">:{{.HostPort}}</td>
<td class="muted">127.0.0.1:{{.ClientPort}}</td> <td class="muted">127.0.0.1:{{.ClientPort}}</td>
<td class="muted">{{.ListenAddr}}{{if and .NoUfw $.UfwEnabled}} · без ufw{{end}}</td> <td class="muted">{{.ListenAddr}}{{if and .NoUfw $.UfwEnabled}} · без ufw{{end}}</td>
<td>{{.Label}}</td>
<td style="text-align:right"> <td style="text-align:right">
<form method="post" action="/forwards/{{.ID}}/delete" style="display:inline" <form method="post" action="/forwards/{{.ID}}/delete" style="display:inline"
onsubmit="return confirm('Удалить правило?')"> onsubmit="return confirm('Удалить правило?')">
@@ -89,6 +90,10 @@
<label>Слушать на хосте</label> <label>Слушать на хосте</label>
<input type="text" name="listen_addr" value="0.0.0.0" style="max-width:160px"> <input type="text" name="listen_addr" value="0.0.0.0" style="max-width:160px">
</div> </div>
<div class="fld" style="flex:1; min-width:200px">
<label>Метка (необязательно)</label>
<input type="text" name="label" maxlength="80" placeholder="для чего это правило">
</div>
<button type="submit">Создать</button> <button type="submit">Создать</button>
</div> </div>
{{if .UfwEnabled}} {{if .UfwEnabled}}