120 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|