No One Can Stop Me Now
This commit is contained in:
243
services/setec-manager/internal/system/firewall.go
Normal file
243
services/setec-manager/internal/system/firewall.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────
|
||||
|
||||
type UFWRule struct {
|
||||
Direction string `json:"direction"` // "in" or "out"
|
||||
Protocol string `json:"protocol"` // "tcp", "udp", or "" for both
|
||||
Port string `json:"port"` // e.g. "22", "80:90"
|
||||
Source string `json:"source"` // IP/CIDR or "any"/"Anywhere"
|
||||
Action string `json:"action"` // "allow", "deny", "reject", "limit"
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// ── Firewall (UFW) ──────────────────────────────────────────────────
|
||||
|
||||
// Status parses `ufw status verbose` and returns the enable state, parsed rules,
|
||||
// and the raw command output.
|
||||
func FirewallStatus() (enabled bool, rules []UFWRule, raw string, err error) {
|
||||
out, cmdErr := exec.Command("ufw", "status", "verbose").CombinedOutput()
|
||||
raw = string(out)
|
||||
if cmdErr != nil {
|
||||
// ufw may return non-zero when inactive; check output
|
||||
if strings.Contains(raw, "Status: inactive") {
|
||||
return false, nil, raw, nil
|
||||
}
|
||||
err = fmt.Errorf("ufw status failed: %w (%s)", cmdErr, raw)
|
||||
return
|
||||
}
|
||||
|
||||
enabled = strings.Contains(raw, "Status: active")
|
||||
|
||||
// Parse rule lines. After the header block, rules look like:
|
||||
// 22/tcp ALLOW IN Anywhere # SSH
|
||||
// 80/tcp ALLOW IN 192.168.1.0/24 # Web
|
||||
// We find lines after the "---" separator.
|
||||
lines := strings.Split(raw, "\n")
|
||||
pastSeparator := false
|
||||
// Match: port/proto (or port) ACTION DIRECTION source # optional comment
|
||||
ruleRegex := regexp.MustCompile(
|
||||
`^(\S+)\s+(ALLOW|DENY|REJECT|LIMIT)\s+(IN|OUT|FWD)?\s*(.+?)(?:\s+#\s*(.*))?$`,
|
||||
)
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "---") {
|
||||
pastSeparator = true
|
||||
continue
|
||||
}
|
||||
if !pastSeparator || trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := ruleRegex.FindStringSubmatch(trimmed)
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
portProto := matches[1]
|
||||
action := strings.ToLower(matches[2])
|
||||
direction := strings.ToLower(matches[3])
|
||||
source := strings.TrimSpace(matches[4])
|
||||
comment := strings.TrimSpace(matches[5])
|
||||
|
||||
if direction == "" {
|
||||
direction = "in"
|
||||
}
|
||||
|
||||
// Split port/protocol
|
||||
var port, proto string
|
||||
if strings.Contains(portProto, "/") {
|
||||
parts := strings.SplitN(portProto, "/", 2)
|
||||
port = parts[0]
|
||||
proto = parts[1]
|
||||
} else {
|
||||
port = portProto
|
||||
}
|
||||
|
||||
// Normalize source
|
||||
if source == "Anywhere" || source == "Anywhere (v6)" {
|
||||
source = "any"
|
||||
}
|
||||
|
||||
rules = append(rules, UFWRule{
|
||||
Direction: direction,
|
||||
Protocol: proto,
|
||||
Port: port,
|
||||
Source: source,
|
||||
Action: action,
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
|
||||
return enabled, rules, raw, nil
|
||||
}
|
||||
|
||||
// FirewallEnable enables UFW with --force to skip the interactive prompt.
|
||||
func FirewallEnable() error {
|
||||
out, err := exec.Command("ufw", "--force", "enable").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ufw enable failed: %w (%s)", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirewallDisable disables UFW.
|
||||
func FirewallDisable() error {
|
||||
out, err := exec.Command("ufw", "disable").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ufw disable failed: %w (%s)", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirewallAddRule constructs and executes a ufw command from the given rule struct.
|
||||
func FirewallAddRule(rule UFWRule) error {
|
||||
if rule.Port == "" {
|
||||
return fmt.Errorf("port is required")
|
||||
}
|
||||
if rule.Action == "" {
|
||||
rule.Action = "allow"
|
||||
}
|
||||
if rule.Protocol == "" {
|
||||
rule.Protocol = "tcp"
|
||||
}
|
||||
if rule.Source == "" || rule.Source == "any" {
|
||||
rule.Source = ""
|
||||
}
|
||||
|
||||
// Validate action
|
||||
switch rule.Action {
|
||||
case "allow", "deny", "reject", "limit":
|
||||
// valid
|
||||
default:
|
||||
return fmt.Errorf("invalid action %q: must be allow, deny, reject, or limit", rule.Action)
|
||||
}
|
||||
|
||||
// Validate protocol
|
||||
switch rule.Protocol {
|
||||
case "tcp", "udp":
|
||||
// valid
|
||||
default:
|
||||
return fmt.Errorf("invalid protocol %q: must be tcp or udp", rule.Protocol)
|
||||
}
|
||||
|
||||
// Validate direction
|
||||
if rule.Direction != "" && rule.Direction != "in" && rule.Direction != "out" {
|
||||
return fmt.Errorf("invalid direction %q: must be in or out", rule.Direction)
|
||||
}
|
||||
|
||||
// Build argument list
|
||||
args := []string{rule.Action}
|
||||
|
||||
// Direction
|
||||
if rule.Direction == "out" {
|
||||
args = append(args, "out")
|
||||
}
|
||||
|
||||
// Source filter
|
||||
if rule.Source != "" {
|
||||
args = append(args, "from", rule.Source)
|
||||
}
|
||||
|
||||
args = append(args, "to", "any", "port", rule.Port, "proto", rule.Protocol)
|
||||
|
||||
// Comment
|
||||
if rule.Comment != "" {
|
||||
args = append(args, "comment", rule.Comment)
|
||||
}
|
||||
|
||||
out, err := exec.Command("ufw", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ufw add rule failed: %w (%s)", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirewallDeleteRule constructs and executes a ufw delete command for the given rule.
|
||||
func FirewallDeleteRule(rule UFWRule) error {
|
||||
if rule.Port == "" {
|
||||
return fmt.Errorf("port is required")
|
||||
}
|
||||
if rule.Action == "" {
|
||||
rule.Action = "allow"
|
||||
}
|
||||
|
||||
// Build the rule specification that matches what was added
|
||||
args := []string{"delete", rule.Action}
|
||||
|
||||
if rule.Direction == "out" {
|
||||
args = append(args, "out")
|
||||
}
|
||||
|
||||
if rule.Source != "" && rule.Source != "any" {
|
||||
args = append(args, "from", rule.Source)
|
||||
}
|
||||
|
||||
portSpec := rule.Port
|
||||
if rule.Protocol != "" {
|
||||
portSpec = rule.Port + "/" + rule.Protocol
|
||||
}
|
||||
args = append(args, portSpec)
|
||||
|
||||
out, err := exec.Command("ufw", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ufw delete rule failed: %w (%s)", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirewallSetDefaults sets the default incoming and outgoing policies.
|
||||
// Valid values are "allow", "deny", "reject".
|
||||
func FirewallSetDefaults(incoming, outgoing string) error {
|
||||
validPolicy := map[string]bool{"allow": true, "deny": true, "reject": true}
|
||||
|
||||
if incoming != "" {
|
||||
if !validPolicy[incoming] {
|
||||
return fmt.Errorf("invalid incoming policy %q: must be allow, deny, or reject", incoming)
|
||||
}
|
||||
out, err := exec.Command("ufw", "default", incoming, "incoming").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting default incoming policy failed: %w (%s)", err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
if outgoing != "" {
|
||||
if !validPolicy[outgoing] {
|
||||
return fmt.Errorf("invalid outgoing policy %q: must be allow, deny, or reject", outgoing)
|
||||
}
|
||||
out, err := exec.Command("ufw", "default", outgoing, "outgoing").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting default outgoing policy failed: %w (%s)", err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user