Files
1Panel/agent/utils/firewall/client/iptables.go
HynoR f35b0deb29 refactor(firewall): extract port forwarding subsystem (#13347)
Port forwarding no longer shares the filter client. FilterClient keeps only
filter capabilities, and forwarding gets its own adapter, service and boot
replay:

- utils/firewall/forwarding holds the provider adapters. firewalld uses native
  forward-port, ufw and iptables share the NAT implementation moved out of
  client/iptables/forward.go.
- service/forwarding.go owns base info, search, operate, enable and replay.
  The API keeps its routes and dispatches on name/type/operate.
- init/firewall replays forwarding through that service instead of loading NAT
  rule files inline.

Also adds 1PANEL_FORWARD to the IptablesOp name enum: the frontend already
sends {"name":"1PANEL_FORWARD","operate":"init-forward"} and the validator
rejected it with 400 before reaching the service. Besides that, the only
observable difference is that a forward-tab search no longer triggers the
port/address record cleanup goroutine on the side.
2026-07-30 14:07:41 +08:00

270 lines
6.2 KiB
Go

package client
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
type Iptables struct{}
func NewIptables() (*Iptables, error) {
return &Iptables{}, nil
}
func (i *Iptables) Name() string {
return "iptables"
}
func (i *Iptables) Status() (bool, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("iptables", "-L", "-n")
if err != nil {
return false, err
}
firstLine := strings.Split(strings.TrimSpace(stdout), "\n")[0]
return strings.Contains(firstLine, "Chain"), nil
}
func (i *Iptables) Start() error {
return nil
}
func (i *Iptables) Stop() error {
return nil
}
func (i *Iptables) Restart() error {
return nil
}
func (i *Iptables) Reload() error {
return nil
}
func (i *Iptables) Version() (string, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("iptables", "--version")
if err != nil {
return "", fmt.Errorf("failed to get iptables version: %w", err)
}
parts := strings.Fields(stdout)
if len(parts) >= 2 {
return strings.TrimPrefix(parts[1], "v"), nil
}
return strings.TrimSpace(stdout), nil
}
func (i *Iptables) ListPort() ([]FireInfo, error) {
var datas []FireInfo
basicRules, err := iptables.ReadFilterRulesByChain(iptables.Chain1PanelBasic)
if err != nil {
return nil, err
}
beforeRules, _ := iptables.ReadFilterRulesByChain(iptables.Chain1PanelBasicBefore)
basicRules = append(basicRules, beforeRules...)
for _, item := range basicRules {
if len(item.DstPort) == 0 {
continue
}
if item.Strategy == "drop" || item.Strategy == "reject" {
item.Strategy = "drop"
}
datas = append(datas, FireInfo{
Chain: item.Chain,
Address: item.SrcIP,
Protocol: item.Protocol,
Port: item.DstPort,
Strategy: item.Strategy,
Family: "ipv4",
})
}
return datas, nil
}
func (i *Iptables) ListAddress() ([]FireInfo, error) {
var datas []FireInfo
basicRules, err := iptables.ReadFilterRulesByChain(iptables.Chain1PanelBasic)
if err != nil {
return nil, err
}
for _, item := range basicRules {
if len(item.DstPort) != 0 || len(item.SrcPort) != 0 {
continue
}
if item.Strategy == "drop" || item.Strategy == "reject" {
item.Strategy = "drop"
}
datas = append(datas, FireInfo{
Address: item.SrcIP,
Strategy: item.Strategy,
Family: "ipv4",
})
}
return datas, nil
}
func (i *Iptables) Port(port FireInfo, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
if len(port.Chain) == 0 {
port.Chain = iptables.Chain1PanelBasic
}
portSpec, err := normalizePortSpec(port.Port)
if err != nil {
return err
}
protocol := port.Protocol
if protocol == "" {
protocol = "tcp"
}
action := "ACCEPT"
if port.Strategy == "drop" {
action = "DROP"
}
ruleArgs := []string{"-p", protocol, "--dport", portSpec, "-j", action}
if operation == "add" {
if err := iptables.AddRule(iptables.FilterTab, port.Chain, ruleArgs...); err != nil {
return err
}
} else {
if err := iptables.DeleteRule(iptables.FilterTab, port.Chain, ruleArgs...); err != nil {
return err
}
}
name := iptables.BasicFileName
if port.Chain == iptables.Chain1PanelBasicBefore {
name = iptables.BasicBeforeFileName
}
if err := iptables.SaveRulesToFile(iptables.FilterTab, port.Chain, name); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
}
return nil
}
func (i *Iptables) RichRules(rule FireInfo, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
if len(rule.Chain) == 0 {
rule.Chain = iptables.Chain1PanelBasic
}
address := strings.TrimSpace(rule.Address)
if strings.EqualFold(address, "Anywhere") {
address = ""
}
action := "ACCEPT"
if rule.Strategy == "drop" {
action = "DROP"
}
var ruleArgs []string
if address != "" {
ruleArgs = append(ruleArgs, "-s", address)
}
protocol := strings.TrimSpace(rule.Protocol)
if rule.Port != "" && protocol == "" {
protocol = "tcp"
}
if protocol != "" {
ruleArgs = append(ruleArgs, "-p", protocol)
}
if rule.Port != "" {
portSegment, err := normalizePortSpec(rule.Port)
if err != nil {
return err
}
if protocol == "" {
return fmt.Errorf("protocol is required when specifying a port")
}
ruleArgs = append(ruleArgs, "--dport", portSegment)
}
ruleArgs = append(ruleArgs, "-j", action)
if operation == "add" {
if err := iptables.AddRule(iptables.FilterTab, rule.Chain, ruleArgs...); err != nil {
return err
}
} else {
if err := iptables.DeleteRule(iptables.FilterTab, rule.Chain, ruleArgs...); err != nil {
return err
}
}
name := iptables.BasicFileName
if rule.Chain == iptables.Chain1PanelBasicBefore {
name = iptables.BasicBeforeFileName
}
if err := iptables.SaveRulesToFile(iptables.FilterTab, rule.Chain, name); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
}
return nil
}
func parsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, fmt.Errorf("invalid port number: %s", portStr)
}
if port < 1 || port > 65535 {
return 0, fmt.Errorf("port out of range: %d", port)
}
return port, nil
}
func normalizePortSpec(port string) (string, error) {
value := strings.TrimSpace(port)
if value == "" {
return "", fmt.Errorf("port is required")
}
separator := ""
if strings.Contains(value, "-") {
separator = "-"
} else if strings.Contains(value, ":") {
separator = ":"
}
if separator != "" {
parts := strings.Split(value, separator)
if len(parts) != 2 {
return "", fmt.Errorf("invalid port range: %s", port)
}
start, err := parsePort(strings.TrimSpace(parts[0]))
if err != nil {
return "", err
}
end, err := parsePort(strings.TrimSpace(parts[1]))
if err != nil {
return "", err
}
if start > end {
return "", fmt.Errorf("invalid port range: %d-%d", start, end)
}
return fmt.Sprintf("%d:%d", start, end), nil
}
single, err := parsePort(value)
if err != nil {
return "", err
}
return fmt.Sprintf("%d", single), nil
}