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