Files
lpra/internal/server/ufw.go
T
2026-10-13 00:25:29 +03:00

120 lines
3.5 KiB
Go

package server
import (
"fmt"
"log"
"os/exec"
"strings"
)
// ufwManager integrates with the host firewall (ufw) when it is installed and
// active. Opening a forward port in the panel also opens it in ufw; the created
// rules are tracked so they can be listed and revoked from Settings.
//
// ufw requires root, so this only works when the server process can run ufw
// (e.g. running as root). When it cannot, integration is simply inactive.
type ufwManager struct {
db *DB
tunnelPort int
}
func newUfwManager(db *DB, tunnelPort int) *ufwManager {
return &ufwManager{db: db, tunnelPort: tunnelPort}
}
// status reports whether ufw is installed, active, and runnable by this process.
func (u *ufwManager) status() (installed, active, runnable bool) {
path, err := exec.LookPath("ufw")
if err != nil {
return false, false, false
}
installed = true
out, err := exec.Command(path, "status").CombinedOutput()
if err != nil {
// Typically a permission error (not root) -> not runnable.
return true, false, false
}
runnable = true
active = strings.Contains(string(out), "Status: active")
return installed, active, runnable
}
// available reports whether we can and should drive ufw now.
func (u *ufwManager) available() bool {
if !u.db.UfwEnabled() {
return false
}
installed, active, runnable := u.status()
return installed && active && runnable
}
// allow opens port/proto in ufw (if integration is available) and records it,
// bound to the given forward rule id (0 = shared/manual).
func (u *ufwManager) allow(port int, proto string, ruleID int64) {
if !u.available() {
return
}
if u.db.HasUfwRule(port, proto) {
return
}
spec := fmt.Sprintf("%d/%s", port, proto)
if out, err := exec.Command("ufw", "allow", spec).CombinedOutput(); err != nil {
log.Printf("ufw allow %s: %v: %s", spec, err, strings.TrimSpace(string(out)))
return
}
_ = u.db.AddUfwRule(port, proto, ruleID)
log.Printf("ufw: allowed %s", spec)
}
// revokeRule deletes one tracked ufw rule from ufw and the tracking table.
func (u *ufwManager) revokeRule(r UfwRule) {
spec := fmt.Sprintf("%d/%s", r.Port, r.Proto)
if out, err := exec.Command("ufw", "delete", "allow", spec).CombinedOutput(); err != nil {
log.Printf("ufw delete allow %s: %v: %s", spec, err, strings.TrimSpace(string(out)))
} else {
log.Printf("ufw: revoked %s", spec)
}
_ = u.db.DeleteUfwRule(r.ID)
}
// revoke removes a ufw rule by its tracking id (manual revoke from Settings).
func (u *ufwManager) revoke(id int64) {
r, err := u.db.GetUfwRule(id)
if err != nil || r == nil {
return
}
u.revokeRule(*r)
}
// revokeForRule removes the ufw rules bound to a specific forward rule. The
// shared tunnel port (rule_id 0) is left alone, since other forwards use it.
func (u *ufwManager) revokeForRule(ruleID int64) {
rules, err := u.db.ListUfwRulesByRule(ruleID)
if err != nil {
return
}
for _, r := range rules {
u.revokeRule(r)
}
}
// allowForward opens the host ports a forward rule needs: its host port (bound
// to the rule so it is revoked with it) plus the shared 8446 tunnel port for
// direct tcp/udp modes (bound to 0 — shared across rules).
func (u *ufwManager) allowForward(rule ForwardRule) {
switch rule.Proto {
case "udp":
u.allow(rule.HostPort, "udp", rule.ID)
if u.tunnelPort > 0 {
u.allow(u.tunnelPort, "udp", 0)
}
case "tcp":
u.allow(rule.HostPort, "tcp", rule.ID)
if u.tunnelPort > 0 {
u.allow(u.tunnelPort, "tcp", 0)
}
default: // http long-poll listener is TCP
u.allow(rule.HostPort, "tcp", rule.ID)
}
}