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.
This commit is contained in:
HynoR
2026-07-30 14:07:41 +08:00
committed by GitHub
parent 33b3eecb95
commit f35b0deb29
23 changed files with 1243 additions and 502 deletions
+1
View File
@@ -42,6 +42,7 @@ var (
fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
forwardingService = service.NewIForwardingService()
iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()
+32 -4
View File
@@ -20,7 +20,15 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
return
}
data, err := firewallService.LoadBaseInfo(req.Name)
var (
data dto.FirewallBaseInfo
err error
)
if req.Name == "forward" {
data, err = forwardingService.LoadBaseInfo()
} else {
data, err = firewallService.LoadBaseInfo(req.Name)
}
if err != nil {
helper.InternalServer(c, err)
return
@@ -43,7 +51,21 @@ func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
return
}
total, list, err := firewallService.SearchWithPage(req)
var (
total int64
list interface{}
err error
)
if req.Type == "forward" {
total, list, err = forwardingService.SearchWithPage(dto.ForwardRuleSearch{
PageInfo: req.PageInfo,
Info: req.Info,
Status: req.Status,
Strategy: req.Strategy,
})
} else {
total, list, err = firewallService.SearchWithPage(req)
}
if err != nil {
helper.InternalServer(c, err)
return
@@ -116,7 +138,7 @@ func (b *BaseApi) OperateForwardRule(c *gin.Context) {
return
}
if err := firewallService.OperateForwardRule(req); err != nil {
if err := forwardingService.Operate(req); err != nil {
helper.InternalServer(c, err)
return
}
@@ -313,7 +335,13 @@ func (b *BaseApi) OperateFilterChain(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := iptablesService.Operate(req); err != nil {
var err error
if req.Operate == "init-forward" {
err = forwardingService.Enable()
} else {
err = iptablesService.Operate(req)
}
if err != nil {
helper.InternalServer(c, err)
return
}
+1 -14
View File
@@ -35,19 +35,6 @@ type PortRuleOperate struct {
Description string `json:"description"`
}
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
} `json:"rules"`
}
type UpdateFirewallDescription struct {
Type string `json:"type"`
Chain string `json:"chain"`
@@ -86,7 +73,7 @@ type BatchRuleOperate struct {
}
type IptablesOp struct {
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC 1PANEL_FORWARD"`
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
}
+43
View File
@@ -0,0 +1,43 @@
package dto
type ForwardRuleSearch struct {
PageInfo
Info string `json:"info"`
Status string `json:"status"`
Strategy string `json:"strategy"`
}
// ForwardRule preserves the existing firewall search response shape while
// keeping forwarding data separate from the filter client model.
type ForwardRule struct {
ID uint `json:"id"`
Chain string `json:"chain"`
Family string `json:"family"`
Address string `json:"address"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Strategy string `json:"strategy"`
Num string `json:"num"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`
Interface string `json:"interface"`
UsedStatus string `json:"usedStatus"`
Description string `json:"description"`
}
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules"`
}
type ForwardRuleOperation struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
}
+3 -98
View File
@@ -3,14 +3,12 @@ package service
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/common"
@@ -28,7 +26,6 @@ type IFirewallService interface {
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
OperateFirewall(req dto.FirewallOperation) error
OperatePortRule(req dto.PortRuleOperate, reload bool) error
OperateForwardRule(req dto.ForwardRuleOperate) error
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
UpdatePortRule(req dto.PortRuleUpdate) error
UpdateAddrRule(req dto.AddrRuleUpdate) error
@@ -84,8 +81,6 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
switch req.Type {
case "port":
rules, err = client.ListPort()
case "forward":
rules, err = client.ListForward()
case "address":
rules, err = client.ListAddress()
}
@@ -317,96 +312,6 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
return nil
}
func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error {
client, err := firewall.NewFirewallClient()
if err != nil {
return err
}
rules, _ := client.ListForward()
i := 0
for _, rule := range rules {
shouldKeep := true
for i := range req.Rules {
reqRule := &req.Rules[i]
if reqRule.TargetIP == "" {
reqRule.TargetIP = "127.0.0.1"
}
if reqRule.Operation == "remove" {
for _, proto := range strings.Split(reqRule.Protocol, "/") {
if reqRule.Port == rule.Port &&
reqRule.TargetPort == rule.TargetPort &&
reqRule.TargetIP == rule.TargetIP &&
proto == rule.Protocol &&
reqRule.Interface == rule.Interface {
shouldKeep = false
break
}
}
}
}
if shouldKeep {
rules[i] = rule
i++
}
}
rules = rules[:i]
for _, rule := range rules {
for _, reqRule := range req.Rules {
if reqRule.Operation == "remove" {
continue
}
for _, proto := range strings.Split(reqRule.Protocol, "/") {
if reqRule.Port == rule.Port &&
reqRule.TargetPort == rule.TargetPort &&
reqRule.TargetIP == rule.TargetIP &&
proto == rule.Protocol &&
reqRule.Interface == rule.Interface {
return buserr.New("ErrRecordExist")
}
}
}
}
sort.SliceStable(req.Rules, func(i, j int) bool {
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
return true
}
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
return false
}
n1, _ := strconv.Atoi(req.Rules[i].Num)
n2, _ := strconv.Atoi(req.Rules[j].Num)
return n1 > n2
})
for _, r := range req.Rules {
for _, p := range strings.Split(r.Protocol, "/") {
if r.TargetIP == "" {
r.TargetIP = "127.0.0.1"
}
if err = client.PortForward(fireClient.Forward{
Num: r.Num,
Protocol: p,
Port: r.Port,
TargetIP: r.TargetIP,
TargetPort: r.TargetPort,
Interface: r.Interface,
}, r.Operation); err != nil {
if req.ForceDelete {
global.LOG.Error(err)
continue
}
return err
}
}
}
return nil
}
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
client, err := firewall.NewFirewallClient()
if err != nil {
@@ -521,7 +426,7 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
return client.Reload()
}
func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.PortRuleOperate) error {
func (u *FirewallService) operatePort(client firewall.FilterClient, req dto.PortRuleOperate) error {
var fireInfo fireClient.FireInfo
if err := copier.Copy(&fireInfo, &req); err != nil {
return err
@@ -589,7 +494,7 @@ func (u *FirewallService) loadPortByApp() []portOfApp {
return datas
}
func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
func (u *FirewallService) cleanUnUsedData(client firewall.FilterClient) {
list, _ := client.ListPort()
addressList, _ := client.ListAddress()
list = append(list, addressList...)
@@ -613,7 +518,7 @@ func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
}
}
func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error {
func (u *FirewallService) addPortsBeforeStart(client firewall.FilterClient) error {
if client.Name() == "iptables" {
isInit, _ := iptables.LoadInitStatus("iptables", "base")
if !isInit {
+1 -1
View File
@@ -138,7 +138,7 @@ func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
}
func syncFirewallClientPortWhiteList(client firewall.FirewallClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
func syncFirewallClientPortWhiteList(client firewall.FilterClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
newPorts := firewallPortWhiteListMap(portWhiteList)
for _, item := range oldPortWhiteList {
+235
View File
@@ -0,0 +1,235 @@
package service
import (
"sort"
"strconv"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
)
type IForwardingService interface {
LoadBaseInfo() (dto.FirewallBaseInfo, error)
SearchWithPage(search dto.ForwardRuleSearch) (int64, interface{}, error)
Operate(req dto.ForwardRuleOperate) error
Enable() error
Replay() error
}
type ForwardingService struct {
adapterFactory func() (forwardClient.Adapter, error)
filterFactory func() (firewall.FilterClient, error)
}
func NewIForwardingService() IForwardingService {
return &ForwardingService{
adapterFactory: newForwardingAdapter,
filterFactory: firewall.NewFirewallClient,
}
}
func newForwardingAdapter() (forwardClient.Adapter, error) {
client, err := firewall.NewFirewallClient()
if err != nil {
return nil, err
}
return forwardClient.NewAdapter(client.Name())
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
baseInfo := dto.FirewallBaseInfo{Version: "-", Name: "-"}
adapter, err := s.adapterFactory()
if err != nil {
global.LOG.Errorf("load forwarding failed, err: %v", err)
return baseInfo, nil
}
filter, err := s.filterFactory()
if err != nil {
global.LOG.Errorf("load firewall status failed, err: %v", err)
return baseInfo, nil
}
baseInfo.IsExist = true
baseInfo.Name = adapter.Name()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.Version, _ = filter.Version()
}()
go func() {
defer wg.Done()
baseInfo.IsActive, _ = filter.Status()
baseInfo.IsInit, baseInfo.IsBind = adapter.InitStatus()
}()
wg.Wait()
return baseInfo, nil
}
func (s *ForwardingService) SearchWithPage(req dto.ForwardRuleSearch) (int64, interface{}, error) {
adapter, err := s.adapterFactory()
if err != nil {
return 0, nil, err
}
rules, err := adapter.List()
if err != nil {
return 0, nil, err
}
if req.Strategy != "" {
return 0, nil, nil
}
var filtered []forwardClient.Rule
for _, rule := range rules {
if req.Info != "" && !strings.Contains(rule.Port, req.Info) &&
!strings.Contains(rule.TargetPort, req.Info) && !strings.Contains(rule.TargetIP, req.Info) {
continue
}
filtered = append(filtered, rule)
}
total := len(filtered)
start, end := (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
return int64(total), make([]dto.ForwardRule, 0), nil
}
if end > total {
end = total
}
pageRules := filtered[start:end]
var items []dto.ForwardRule
if pageRules != nil {
items = make([]dto.ForwardRule, 0, len(pageRules))
}
for _, rule := range pageRules {
items = append(items, dto.ForwardRule{
Num: rule.Num,
Protocol: rule.Protocol,
Port: rule.Port,
TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort,
Interface: rule.Interface,
})
}
return int64(total), items, nil
}
func (s *ForwardingService) Operate(req dto.ForwardRuleOperate) error {
adapter, err := s.adapterFactory()
if err != nil {
return err
}
rules, _ := adapter.List()
kept := rules[:0]
for _, rule := range rules {
shouldKeep := true
for i := range req.Rules {
reqRule := &req.Rules[i]
if reqRule.TargetIP == "" {
reqRule.TargetIP = "127.0.0.1"
}
if reqRule.Operation == "remove" && requestMatchesForwardRule(*reqRule, rule) {
shouldKeep = false
break
}
}
if shouldKeep {
kept = append(kept, rule)
}
}
for _, rule := range kept {
for _, reqRule := range req.Rules {
if reqRule.Operation != "remove" && requestMatchesForwardRule(reqRule, rule) {
return buserr.New("ErrRecordExist")
}
}
}
sort.SliceStable(req.Rules, func(i, j int) bool {
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
return true
}
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
return false
}
n1, _ := strconv.Atoi(req.Rules[i].Num)
n2, _ := strconv.Atoi(req.Rules[j].Num)
return n1 > n2
})
for _, rule := range req.Rules {
for _, protocol := range strings.Split(rule.Protocol, "/") {
targetIP := rule.TargetIP
if targetIP == "" {
targetIP = "127.0.0.1"
}
err := adapter.Operate(forwardClient.Rule{
Num: rule.Num,
Protocol: protocol,
Port: rule.Port,
TargetIP: targetIP,
TargetPort: rule.TargetPort,
Interface: rule.Interface,
}, rule.Operation)
if err == nil {
continue
}
if req.ForceDelete {
global.LOG.Error(err)
continue
}
return err
}
}
return nil
}
func requestMatchesForwardRule(req dto.ForwardRuleOperation, rule forwardClient.Rule) bool {
for _, protocol := range strings.Split(req.Protocol, "/") {
if req.Port == rule.Port && req.TargetPort == rule.TargetPort && req.TargetIP == rule.TargetIP &&
protocol == rule.Protocol && req.Interface == rule.Interface {
return true
}
}
return false
}
func (s *ForwardingService) Enable() error {
adapter, err := s.adapterFactory()
if err != nil {
return err
}
if err := adapter.Enable(); err != nil {
return err
}
if adapter.Name() != "firewalld" {
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
}
return nil
}
func (s *ForwardingService) Replay() error {
adapter, err := s.adapterFactory()
if err != nil {
return err
}
if err := adapter.Replay(); err != nil {
return err
}
if adapter.Name() == "firewalld" {
return nil
}
status, _ := settingRepo.GetValueByKey("IptablesForwardStatus")
if status == constant.StatusEnable {
return adapter.Enable()
}
return nil
}
@@ -0,0 +1,152 @@
package service
import (
"encoding/json"
"errors"
"reflect"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/go-playground/validator/v10"
)
type forwardingCall struct {
rule forwardClient.Rule
operation string
}
type fakeForwardingAdapter struct {
name string
rules []forwardClient.Rule
listErr error
operateErr error
calls []forwardingCall
}
func (f *fakeForwardingAdapter) Name() string { return f.name }
func (f *fakeForwardingAdapter) List() ([]forwardClient.Rule, error) {
return append([]forwardClient.Rule(nil), f.rules...), f.listErr
}
func (f *fakeForwardingAdapter) Operate(rule forwardClient.Rule, operation string) error {
f.calls = append(f.calls, forwardingCall{rule: rule, operation: operation})
return f.operateErr
}
func (f *fakeForwardingAdapter) Enable() error { return nil }
func (f *fakeForwardingAdapter) InitStatus() (bool, bool) { return true, true }
func (f *fakeForwardingAdapter) Replay() error { return nil }
func forwardingServiceWithAdapter(adapter forwardClient.Adapter) *ForwardingService {
return &ForwardingService{
adapterFactory: func() (forwardClient.Adapter, error) { return adapter, nil },
filterFactory: firewall.NewFirewallClient,
}
}
func TestForwardingAndFilterInterfacesAreSeparated(t *testing.T) {
filterType := reflect.TypeOf((*firewall.FilterClient)(nil)).Elem()
for _, method := range []string{"ListForward", "PortForward", "EnableForward"} {
if _, ok := filterType.MethodByName(method); ok {
t.Fatalf("filter interface still exposes %s", method)
}
}
firewallServiceType := reflect.TypeOf((*IFirewallService)(nil)).Elem()
if _, ok := firewallServiceType.MethodByName("OperateForwardRule"); ok {
t.Fatal("firewall service still owns forwarding writes")
}
forwardingServiceType := reflect.TypeOf((*IForwardingService)(nil)).Elem()
for _, method := range []string{"LoadBaseInfo", "SearchWithPage", "Operate", "Enable", "Replay"} {
if _, ok := forwardingServiceType.MethodByName(method); !ok {
t.Fatalf("forwarding service missing %s", method)
}
}
}
func TestForwardingInitRequestContract(t *testing.T) {
req := dto.IptablesOp{Name: "1PANEL_FORWARD", Operate: "init-forward"}
if err := validator.New().Struct(req); err != nil {
t.Fatalf("frontend forwarding initialization request must remain valid: %v", err)
}
}
func TestForwardingSearchPreservesAPIShapeAndPagination(t *testing.T) {
adapter := &fakeForwardingAdapter{name: "iptables", rules: []forwardClient.Rule{
{Num: "1", Protocol: "tcp", Port: "8080", TargetIP: "10.0.0.2", TargetPort: "80", Interface: "eth0"},
{Num: "2", Protocol: "udp", Port: "5353", TargetIP: "127.0.0.1", TargetPort: "53"},
}}
service := forwardingServiceWithAdapter(adapter)
total, value, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 10}, Info: "10.0.0.2"})
if err != nil {
t.Fatal(err)
}
if total != 1 {
t.Fatalf("got total %d want 1", total)
}
items, ok := value.([]dto.ForwardRule)
if !ok || len(items) != 1 || items[0].Port != "8080" {
t.Fatalf("unexpected items: %#v", value)
}
data, err := json.Marshal(items[0])
if err != nil {
t.Fatal(err)
}
var fields map[string]interface{}
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatal(err)
}
wantFields := []string{"id", "chain", "family", "address", "port", "protocol", "strategy", "num", "targetIP", "targetPort", "interface", "usedStatus", "description"}
for _, field := range wantFields {
if _, ok := fields[field]; !ok {
t.Fatalf("forward response dropped compatibility field %q: %s", field, data)
}
}
}
func TestForwardingOperatePreservesDuplicateAndOrderingContracts(t *testing.T) {
existing := &fakeForwardingAdapter{name: "ufw", rules: []forwardClient.Rule{
{Protocol: "tcp", Port: "8080", TargetIP: "127.0.0.1", TargetPort: "80"},
}}
service := forwardingServiceWithAdapter(existing)
err := service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{{
Operation: "add", Protocol: "tcp", Port: "8080", TargetPort: "80",
}}})
if err == nil {
t.Fatal("duplicate forwarding rule must be rejected")
}
if len(existing.calls) != 0 {
t.Fatalf("duplicate check wrote forwarding state: %#v", existing.calls)
}
adapter := &fakeForwardingAdapter{name: "iptables"}
service = forwardingServiceWithAdapter(adapter)
err = service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{
{Operation: "add", Protocol: "tcp/udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"},
{Operation: "remove", Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"},
{Operation: "remove", Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"},
}})
if err != nil {
t.Fatal(err)
}
want := []forwardingCall{
{operation: "remove", rule: forwardClient.Rule{Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"}},
{operation: "remove", rule: forwardClient.Rule{Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"}},
{operation: "add", rule: forwardClient.Rule{Protocol: "tcp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
{operation: "add", rule: forwardClient.Rule{Protocol: "udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
}
if !reflect.DeepEqual(adapter.calls, want) {
t.Fatalf("operation order changed\ngot %#v\nwant %#v", adapter.calls, want)
}
}
func TestForwardingSearchReturnsAdapterError(t *testing.T) {
wantErr := errors.New("list failed")
service := forwardingServiceWithAdapter(&fakeForwardingAdapter{name: "firewalld", listErr: wantErr})
_, _, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 20}})
if !errors.Is(err, wantErr) {
t.Fatalf("got %v want %v", err, wantErr)
}
}
-7
View File
@@ -11,7 +11,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
@@ -195,12 +194,6 @@ func (s *IptablesService) Operate(req dto.IptablesOp) error {
}
_ = settingRepo.Update("IptablesStatus", constant.StatusEnable)
return nil
case "init-forward":
if err := client.EnableIptablesForward(); err != nil {
return err
}
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
return nil
case "init-advance":
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelInput); err != nil {
return err
+4 -25
View File
@@ -10,7 +10,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
firewallClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
@@ -25,35 +24,15 @@ func Init() {
return
}
clientName := client.Name()
settingRepo := repo.NewISettingRepo()
if clientName == "ufw" || clientName == "iptables" {
if err := iptables.LoadRulesFromFile(iptables.FilterTab, iptables.Chain1PanelForward, iptables.ForwardFileName); err != nil {
global.LOG.Errorf("load forward rules from file failed, err: %v", err)
return
}
if err := iptables.LoadRulesFromFile(iptables.NatTab, iptables.Chain1PanelPreRouting, iptables.ForwardFileName1); err != nil {
global.LOG.Errorf("load prerouting rules from file failed, err: %v", err)
return
}
if err := iptables.LoadRulesFromFile(iptables.NatTab, iptables.Chain1PanelPostRouting, iptables.ForwardFileName2); err != nil {
global.LOG.Errorf("load postrouting rules from file failed, err: %v", err)
return
}
global.LOG.Infof("loaded iptables rules for forward from file successfully")
iptablesForwardStatus, _ := settingRepo.GetValueByKey("IptablesForwardStatus")
if iptablesForwardStatus == constant.StatusEnable {
if err := firewallClient.EnableIptablesForward(); err != nil {
global.LOG.Errorf("enable iptables forward failed, err: %v", err)
return
}
}
if err := service.NewIForwardingService().Replay(); err != nil {
global.LOG.Errorf("replay forwarding rules failed, err: %v", err)
return
}
if clientName != "iptables" {
return
}
settingRepo := repo.NewISettingRepo()
if err := iptables.LoadRulesFromFile(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.BasicBeforeFileName); err != nil {
global.LOG.Errorf("load basic before rules from file failed, err: %v", err)
return
+5 -7
View File
@@ -12,8 +12,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
)
type FirewallClient interface {
Name() string // ufw firewalld
// FilterClient is the filter capability surface; port forwarding lives in its
// own adapter and is no longer reachable from here.
type FilterClient interface {
Name() string // ufw firewalld iptables
Start() error
Stop() error
Restart() error
@@ -22,17 +24,13 @@ type FirewallClient interface {
Version() (string, error)
ListPort() ([]client.FireInfo, error)
ListForward() ([]client.FireInfo, error)
ListAddress() ([]client.FireInfo, error)
Port(port client.FireInfo, operation string) error
RichRules(rule client.FireInfo, operation string) error
PortForward(info client.Forward, operation string) error
EnableForward() error
}
func NewFirewallClient() (FirewallClient, error) {
func NewFirewallClient() (FilterClient, error) {
firewalld := cmd.Which("firewalld")
ufw := cmd.Which("ufw")
-62
View File
@@ -6,7 +6,6 @@ import (
"sync"
"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/controller"
)
@@ -108,34 +107,6 @@ func (f *Firewall) ListPort() ([]FireInfo, error) {
return datas, nil
}
func (f *Firewall) ListForward() ([]FireInfo, error) {
if err := f.EnableForward(); err != nil {
global.LOG.Errorf("init port forward failed, err: %v", err)
}
stdout, err := cmd.NewCommandMgr().RunWithStdout("firewall-cmd", "--zone=public", "--list-forward-ports")
if err != nil {
return nil, err
}
var datas []FireInfo
for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimSpace(line)
parts := strings.Split(line, ":")
if len(parts) < 4 {
continue
}
if parts[3] == "toaddr=" {
parts[3] = "127.0.0.1"
}
datas = append(datas, FireInfo{
Port: strings.TrimPrefix(parts[0], "port="),
Protocol: strings.TrimPrefix(parts[1], "proto="),
TargetIP: strings.TrimPrefix(parts[3], "toaddr="),
TargetPort: strings.TrimPrefix(parts[2], "toport="),
})
}
return datas, nil
}
func (f *Firewall) ListAddress() ([]FireInfo, error) {
stdout, err := cmd.NewCommandMgr().RunWithStdout("firewall-cmd", "--zone=public", "--list-rich-rules")
if err != nil {
@@ -196,24 +167,6 @@ func (f *Firewall) RichRules(rule FireInfo, operation string) error {
return nil
}
func (f *Firewall) PortForward(info Forward, operation string) error {
if cmd.CheckIllegal(operation, info.Port, info.Protocol, info.TargetIP, info.TargetPort) {
return buserr.New("ErrCmdIllegal")
}
forwardRule := fmt.Sprintf("--%s-forward-port=port=%s:proto=%s:toport=%s", operation, info.Port, info.Protocol, info.TargetPort)
if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" {
forwardRule = fmt.Sprintf("--%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s", operation, info.Port, info.Protocol, info.TargetIP, info.TargetPort)
}
if err := cmd.NewCommandMgr().Run("firewall-cmd", "--zone=public", forwardRule, "--permanent"); err != nil {
return fmt.Errorf("%s port forward failed, %s", operation, err)
}
if err := f.Reload(); err != nil {
return err
}
return nil
}
func (f *Firewall) loadInfo(line string) FireInfo {
var itemRule FireInfo
ruleInfo := strings.Split(strings.ReplaceAll(line, "\"", ""), " ")
@@ -235,18 +188,3 @@ func (f *Firewall) loadInfo(line string) FireInfo {
}
return itemRule
}
func (f *Firewall) EnableForward() error {
stdout, err := cmd.NewCommandMgr().RunWithStdout("firewall-cmd", "--zone=public", "--query-masquerade")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(stdout), "no") {
if err := cmd.NewCommandMgr().Run("firewall-cmd", "--zone=public", "--add-masquerade", "--permanent"); err != nil {
return err
}
return f.Reload()
}
return err
}
return nil
}
-9
View File
@@ -17,12 +17,3 @@ type FireInfo struct {
UsedStatus string `json:"usedStatus"`
Description string `json:"description"`
}
type Forward struct {
Num string `json:"num"`
Protocol string `json:"protocol"`
Port string `json:"port"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`
Interface string `json:"interface"`
}
-91
View File
@@ -2,7 +2,6 @@ package client
import (
"fmt"
"os"
"strconv"
"strings"
"time"
@@ -219,96 +218,6 @@ func (i *Iptables) RichRules(rule FireInfo, operation string) error {
return nil
}
func (i *Iptables) PortForward(info Forward, operation string) error {
return iptablesPortForward(info, operation)
}
func (i *Iptables) EnableForward() error {
return EnableIptablesForward()
}
func (i *Iptables) ListForward() ([]FireInfo, error) {
return iptablesListForward()
}
func EnableIptablesForward() error {
if err := cmd.WriteFileWithOptionalSudo("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644); err != nil {
return fmt.Errorf("failed to enable IP forwarding: %w", err)
}
if data, err := os.ReadFile("/etc/sysctl.conf"); err == nil {
if !strings.Contains(string(data), "net.ipv4.ip_forward") {
content := strings.TrimRight(string(data), "\n") + "\nnet.ipv4.ip_forward = 1\n"
_ = cmd.WriteFileWithOptionalSudo("/etc/sysctl.conf", []byte(content), 0644)
}
}
_ = cmd.NewCommandMgr().RunWithOptionalSudo("sysctl", "-p")
if err := iptables.AddChainWithAppend(iptables.NatTab, "PREROUTING", iptables.Chain1PanelPreRouting); err != nil {
return err
}
if err := iptables.AddChainWithAppend(iptables.NatTab, "POSTROUTING", iptables.Chain1PanelPostRouting); err != nil {
return err
}
if err := iptables.AddChainWithAppend(iptables.FilterTab, "FORWARD", iptables.Chain1PanelForward); err != nil {
return err
}
return nil
}
func iptablesPortForward(info Forward, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
if info.Protocol == "" || info.Port == "" || info.TargetPort == "" {
return fmt.Errorf("protocol, port, and target port are required")
}
if operation == "add" {
if err := iptables.AddForward(info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface, true); err != nil {
return err
}
} else {
if err := iptables.DeleteForward(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface); err != nil {
return err
}
}
forwardPersistence()
return nil
}
func forwardPersistence() {
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelForward, iptables.ForwardFileName); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelForward, err)
}
if err := iptables.SaveRulesToFile(iptables.NatTab, iptables.Chain1PanelPreRouting, iptables.ForwardFileName1); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelPreRouting, err)
}
if err := iptables.SaveRulesToFile(iptables.NatTab, iptables.Chain1PanelPostRouting, iptables.ForwardFileName2); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelPostRouting, err)
}
}
func iptablesListForward() ([]FireInfo, error) {
natList, err := iptables.ListForward(iptables.Chain1PanelPreRouting)
if err != nil {
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
}
var datas []FireInfo
for _, nat := range natList {
datas = append(datas, FireInfo{
Num: nat.Num,
Protocol: nat.Protocol,
Port: strings.TrimPrefix(nat.SrcPort, ":"),
TargetIP: nat.Destination,
TargetPort: strings.TrimPrefix(nat.DestPort, ":"),
Interface: nat.InIface,
})
}
return datas, nil
}
func parsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
@@ -11,9 +11,6 @@ import (
)
const (
Chain1PanelPreRouting = "1PANEL_PREROUTING"
Chain1PanelPostRouting = "1PANEL_POSTROUTING"
Chain1PanelForward = "1PANEL_FORWARD"
ChainInput = "INPUT"
ChainOutput = "OUTPUT"
Chain1PanelInput = "1PANEL_INPUT"
+1 -37
View File
@@ -2,7 +2,6 @@ package iptables
import (
"fmt"
"os"
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -127,7 +126,7 @@ func LoadInitStatus(clientName, tab string) (bool, bool) {
if clientName == "firewalld" {
return true, true
}
if clientName == "ufw" && tab != "forward" {
if clientName == "ufw" {
return true, true
}
switch tab {
@@ -167,41 +166,6 @@ func LoadInitStatus(clientName, tab string) (bool, bool) {
fmt.Sprintf("-A %s -j %s", ChainOutput, Chain1PanelOutput),
}
return checkWithInitAndBind(initRules, bindRules, lines)
case "forward":
data, err := os.ReadFile("/proc/sys/net/ipv4/ip_forward")
if err != nil {
global.LOG.Errorf("check /proc/sys/net/ipv4/ip_forward failed, err: %v", err)
return false, false
}
if strings.TrimSpace(string(data)) == "0" {
return false, false
}
natRules, err := RunWithStd(NatTab, "-S")
if err != nil {
return false, false
}
lines := strings.Split(natRules, "\n")
initRules := []string{
"-N " + Chain1PanelPreRouting,
"-N " + Chain1PanelPostRouting,
}
bindRules := []string{
fmt.Sprintf("-A PREROUTING -j %s", Chain1PanelPreRouting),
fmt.Sprintf("-A POSTROUTING -j %s", Chain1PanelPostRouting),
}
isNatInit, isNatBind := checkWithInitAndBind(initRules, bindRules, lines)
if !isNatInit {
return false, false
}
filterRules, err := RunWithStd(FilterTab, "-S")
if err != nil {
return false, false
}
filterLines := strings.Split(filterRules, "\n")
filterInitRules := []string{"-N " + Chain1PanelForward}
filterBindRules := []string{fmt.Sprintf("-A FORWARD -j %s", Chain1PanelForward)}
isFilterInit, isFilterBind := checkWithInitAndBind(filterInitRules, filterBindRules, filterLines)
return isNatInit && isFilterInit, isNatBind && isFilterBind
default:
return false, false
}
@@ -1,129 +0,0 @@
package iptables
import (
"strings"
)
func AddForward(protocol, srcPort, dest, destPort, iface string, save bool) error {
srcPort = strings.ReplaceAll(srcPort, "-", ":")
itemDstPort := strings.ReplaceAll(destPort, "-", ":")
if dest != "" && dest != "127.0.0.1" && dest != "localhost" {
args := []string{"-A", Chain1PanelPreRouting}
if iface != "" {
args = append(args, "-i", iface)
}
args = append(args, "-p", protocol, "--dport", srcPort, "-j", "DNAT", "--to-destination", dest+":"+destPort)
if err := Run(NatTab, args...); err != nil {
return err
}
if err := Run(NatTab, "-A", Chain1PanelPostRouting, "-d", dest, "-p", protocol, "--dport", itemDstPort, "-j", "MASQUERADE"); err != nil {
return err
}
if err := Run(FilterTab, "-A", Chain1PanelForward, "-d", dest, "-p", protocol, "--dport", itemDstPort, "-j", "ACCEPT"); err != nil {
return err
}
if err := Run(FilterTab, "-A", Chain1PanelForward, "-s", dest, "-p", protocol, "--sport", itemDstPort, "-j", "ACCEPT"); err != nil {
return err
}
} else {
args := []string{"-A", Chain1PanelPreRouting}
if iface != "" {
args = append(args, "-i", iface)
}
args = append(args, "-p", protocol, "--dport", srcPort, "-j", "REDIRECT", "--to-port", destPort)
if err := Run(NatTab, args...); err != nil {
return err
}
}
return nil
}
func DeleteForward(num string, protocol, srcPort, dest, destPort, iface string) error {
itemDstPort := strings.ReplaceAll(destPort, "-", ":")
if err := Run(NatTab, "-D", Chain1PanelPreRouting, num); err != nil {
return err
}
if dest != "" && dest != "127.0.0.1" && dest != "localhost" {
if err := Run(NatTab, "-D", Chain1PanelPostRouting, "-d", dest, "-p", protocol, "--dport", itemDstPort, "-j", "MASQUERADE"); err != nil {
return err
}
if err := Run(FilterTab, "-D", Chain1PanelForward, "-d", dest, "-p", protocol, "--dport", itemDstPort, "-j", "ACCEPT"); err != nil {
return err
}
if err := Run(FilterTab, "-D", Chain1PanelForward, "-s", dest, "-p", protocol, "--sport", itemDstPort, "-j", "ACCEPT"); err != nil {
return err
}
}
return nil
}
func ListForward(chain ...string) ([]IptablesNatInfo, error) {
if len(chain) == 0 {
chain = append(chain, Chain1PanelPreRouting)
}
stdout, err := RunWithStd(NatTab, "-nvL", chain[0], "--line-numbers")
if err != nil {
return nil, err
}
var forwardList []IptablesNatInfo
lines := strings.Split(stdout, "\n")
for i := 0; i < len(lines); i++ {
fields := strings.Fields(lines[i])
if len(fields) < 13 {
continue
}
item := IptablesNatInfo{
Num: fields[0],
Protocol: loadProtocol(fields[4]),
InIface: fields[6],
OutIface: fields[7],
Source: fields[8],
SrcPort: loadNatSrcPort(fields[11]),
}
if len(fields) == 15 && fields[13] == "ports" {
item.DestPort = fields[14]
}
if len(fields) == 13 && strings.HasPrefix(fields[12], "to:") {
parts := strings.Split(fields[12], ":")
if len(parts) > 2 {
item.DestPort = parts[2]
item.Destination = parts[1]
}
}
if len(item.Destination) == 0 {
item.Destination = "127.0.0.1"
}
forwardList = append(forwardList, item)
}
return forwardList, nil
}
func loadNatSrcPort(portStr string) string {
var portItem string
if strings.Contains(portStr, "dpt:") {
portItem = strings.ReplaceAll(portStr, "dpt:", "")
}
if strings.Contains(portStr, "dpts:") {
portItem = strings.ReplaceAll(portStr, "dpts:", "")
}
portItem = strings.ReplaceAll(portItem, ":", "-")
return portItem
}
type IptablesNatInfo struct {
Num string `json:"num"`
Protocol string `json:"protocol"`
InIface string `json:"inIface"`
OutIface string `json:"outIface"`
Source string `json:"source"`
Destination string `json:"destination"`
SrcPort string `json:"srcPort"`
DestPort string `json:"destPort"`
}
@@ -18,9 +18,6 @@ const (
BasicAfterFileName = "1panel_basic_after.rules"
InputFileName = "1panel_input.rules"
OutputFileName = "1panel_out.rules"
ForwardFileName = "1panel_forward.rules"
ForwardFileName1 = "1panel_forward_pre.rules"
ForwardFileName2 = "1panel_forward_post.rules"
)
func SaveRulesToFile(tab, chain, fileName string) error {
-12
View File
@@ -203,18 +203,6 @@ func (f *Ufw) RichRules(rule FireInfo, operation string) error {
return nil
}
func (f *Ufw) PortForward(info Forward, operation string) error {
return iptablesPortForward(info, operation)
}
func (f *Ufw) EnableForward() error {
return EnableIptablesForward()
}
func (f *Ufw) ListForward() ([]FireInfo, error) {
return iptablesListForward()
}
func (f *Ufw) loadInfo(line string, fireType string) FireInfo {
fields := strings.Fields(line)
var itemInfo FireInfo
@@ -0,0 +1,46 @@
package forwarding
import (
"errors"
)
const (
ChainPreRouting = "1PANEL_PREROUTING"
ChainPostRouting = "1PANEL_POSTROUTING"
ChainForward = "1PANEL_FORWARD"
ForwardFile = "1panel_forward.rules"
PreRoutingFile = "1panel_forward_pre.rules"
PostRoutingFile = "1panel_forward_post.rules"
)
type Rule struct {
Num string
Protocol string
Port string
TargetIP string
TargetPort string
Interface string
}
// Adapter is the complete provider-specific forwarding surface. Filter
// clients intentionally do not implement any of these methods.
type Adapter interface {
Name() string
List() ([]Rule, error)
Operate(rule Rule, operation string) error
Enable() error
InitStatus() (bool, bool)
Replay() error
}
func NewAdapter(provider string) (Adapter, error) {
switch provider {
case "firewalld":
return newFirewalldAdapter(), nil
case "ufw", "iptables":
return newLegacyNATAdapter(provider), nil
default:
return nil, errors.New("unsupported forwarding provider: " + provider)
}
}
@@ -0,0 +1,274 @@
package forwarding
import (
"errors"
"os"
"reflect"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
type commandCall struct {
name string
args []string
}
type fakeFirewalldRunner struct {
calls []commandCall
stdout map[string]string
errors map[string]error
}
func (f *fakeFirewalldRunner) Run(name string, args ...string) error {
f.calls = append(f.calls, commandCall{name: name, args: append([]string(nil), args...)})
return f.errors[commandKey(name, args...)]
}
func (f *fakeFirewalldRunner) RunWithStdout(name string, args ...string) (string, error) {
f.calls = append(f.calls, commandCall{name: name, args: append([]string(nil), args...)})
key := commandKey(name, args...)
return f.stdout[key], f.errors[key]
}
func commandKey(name string, args ...string) string {
return strings.Join(append([]string{name}, args...), " ")
}
func TestForwardingAdapterFactoryContract(t *testing.T) {
for _, name := range []string{"firewalld", "ufw", "iptables"} {
adapter, err := NewAdapter(name)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if adapter.Name() != name {
t.Fatalf("got adapter %q want %q", adapter.Name(), name)
}
}
if _, err := NewAdapter("unknown"); err == nil {
t.Fatal("unknown forwarding provider must be rejected")
}
}
func TestFirewalldForwardingContract(t *testing.T) {
runner := &fakeFirewalldRunner{stdout: map[string]string{
"firewall-cmd --zone=public --query-masquerade": "yes\n",
"firewall-cmd --zone=public --list-forward-ports": "port=8080:proto=tcp:toport=80:toaddr=10.0.0.2\nport=8443:proto=tcp:toport=443:toaddr=\ninvalid\n",
}, errors: map[string]error{}}
adapter := &firewalldAdapter{runner: runner}
rules, err := adapter.List()
if err != nil {
t.Fatal(err)
}
wantRules := []Rule{
{Port: "8080", Protocol: "tcp", TargetIP: "10.0.0.2", TargetPort: "80"},
{Port: "8443", Protocol: "tcp", TargetIP: "127.0.0.1", TargetPort: "443"},
}
if !reflect.DeepEqual(rules, wantRules) {
t.Fatalf("got %#v want %#v", rules, wantRules)
}
remote := Rule{Port: "8080", Protocol: "tcp", TargetIP: "10.0.0.2", TargetPort: "80"}
if err := adapter.Operate(remote, "add"); err != nil {
t.Fatal(err)
}
wantArgs := []string{"--zone=public", "--add-forward-port=port=8080:proto=tcp:toaddr=10.0.0.2:toport=80", "--permanent"}
assertArgs(t, runner.calls[2], "firewall-cmd", wantArgs)
assertArgs(t, runner.calls[3], "firewall-cmd", []string{"--reload"})
localArgs := buildFirewalldForwardArgs(Rule{Port: "8443", Protocol: "tcp", TargetIP: "127.0.0.1", TargetPort: "443"}, "remove")
wantLocal := []string{"--zone=public", "--remove-forward-port=port=8443:proto=tcp:toport=443", "--permanent"}
if !reflect.DeepEqual(localArgs, wantLocal) {
t.Fatalf("got %#v want %#v", localArgs, wantLocal)
}
}
func TestFirewalldEnableMasqueradeContract(t *testing.T) {
query := "firewall-cmd --zone=public --query-masquerade"
runner := &fakeFirewalldRunner{
stdout: map[string]string{query: "no\n"},
errors: map[string]error{query: errors.New("exit status 1")},
}
adapter := &firewalldAdapter{runner: runner}
if err := adapter.Enable(); err != nil {
t.Fatal(err)
}
want := []commandCall{
{name: "firewall-cmd", args: []string{"--zone=public", "--query-masquerade"}},
{name: "firewall-cmd", args: []string{"--zone=public", "--add-masquerade", "--permanent"}},
{name: "firewall-cmd", args: []string{"--reload"}},
}
if !reflect.DeepEqual(runner.calls, want) {
t.Fatalf("got %#v want %#v", runner.calls, want)
}
}
type backendCall struct {
method string
table string
args []string
}
type fakeLegacyBackend struct {
calls []backendCall
stdout map[string]string
err error
}
func (f *fakeLegacyBackend) Run(table string, args ...string) error {
f.calls = append(f.calls, backendCall{method: "run", table: table, args: append([]string(nil), args...)})
return f.err
}
func (f *fakeLegacyBackend) RunWithStd(table string, args ...string) (string, error) {
f.calls = append(f.calls, backendCall{method: "stdout", table: table, args: append([]string(nil), args...)})
return f.stdout[commandKey(table, args...)], f.err
}
func (f *fakeLegacyBackend) AddChainWithAppend(table, parentChain, chain string) error {
f.calls = append(f.calls, backendCall{method: "add-chain", table: table, args: []string{parentChain, chain}})
return f.err
}
func (f *fakeLegacyBackend) SaveRulesToFile(table, chain, fileName string) error {
f.calls = append(f.calls, backendCall{method: "save", table: table, args: []string{chain, fileName}})
return f.err
}
func (f *fakeLegacyBackend) LoadRulesFromFile(table, chain, fileName string) error {
f.calls = append(f.calls, backendCall{method: "load", table: table, args: []string{chain, fileName}})
return f.err
}
type fileWrite struct {
name string
data string
}
type fakeForwardingSystem struct {
reads map[string][]byte
writes []fileWrite
runs []commandCall
}
func (f *fakeForwardingSystem) ReadFile(name string) ([]byte, error) {
data, ok := f.reads[name]
if !ok {
return nil, os.ErrNotExist
}
return data, nil
}
func (f *fakeForwardingSystem) WriteFile(name string, data []byte, _ os.FileMode) error {
f.writes = append(f.writes, fileWrite{name: name, data: string(data)})
return nil
}
func (f *fakeForwardingSystem) RunWithOptionalSudo(name string, args ...string) error {
f.runs = append(f.runs, commandCall{name: name, args: append([]string(nil), args...)})
return nil
}
func TestLegacyNATAddDeleteAndPersistenceContract(t *testing.T) {
backend := &fakeLegacyBackend{stdout: map[string]string{}}
adapter := &legacyNATAdapter{provider: "ufw", backend: backend, system: &fakeForwardingSystem{}}
rule := Rule{Num: "3", Protocol: "tcp", Port: "8080-8081", TargetIP: "10.0.0.2", TargetPort: "80-81", Interface: "eth0"}
if err := adapter.Operate(rule, "add"); err != nil {
t.Fatal(err)
}
wantAdd := []backendCall{
{method: "run", table: iptables.NatTab, args: []string{"-A", ChainPreRouting, "-i", "eth0", "-p", "tcp", "--dport", "8080:8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80-81"}},
{method: "run", table: iptables.NatTab, args: []string{"-A", ChainPostRouting, "-d", "10.0.0.2", "-p", "tcp", "--dport", "80:81", "-j", "MASQUERADE"}},
{method: "run", table: iptables.FilterTab, args: []string{"-A", ChainForward, "-d", "10.0.0.2", "-p", "tcp", "--dport", "80:81", "-j", "ACCEPT"}},
{method: "run", table: iptables.FilterTab, args: []string{"-A", ChainForward, "-s", "10.0.0.2", "-p", "tcp", "--sport", "80:81", "-j", "ACCEPT"}},
{method: "save", table: iptables.FilterTab, args: []string{ChainForward, ForwardFile}},
{method: "save", table: iptables.NatTab, args: []string{ChainPreRouting, PreRoutingFile}},
{method: "save", table: iptables.NatTab, args: []string{ChainPostRouting, PostRoutingFile}},
}
if !reflect.DeepEqual(backend.calls, wantAdd) {
t.Fatalf("add transcript changed\ngot %#v\nwant %#v", backend.calls, wantAdd)
}
backend.calls = nil
if err := adapter.Operate(rule, "remove"); err != nil {
t.Fatal(err)
}
wantRemovePrefix := []backendCall{
{method: "run", table: iptables.NatTab, args: []string{"-D", ChainPreRouting, "3"}},
{method: "run", table: iptables.NatTab, args: []string{"-D", ChainPostRouting, "-d", "10.0.0.2", "-p", "tcp", "--dport", "80:81", "-j", "MASQUERADE"}},
{method: "run", table: iptables.FilterTab, args: []string{"-D", ChainForward, "-d", "10.0.0.2", "-p", "tcp", "--dport", "80:81", "-j", "ACCEPT"}},
{method: "run", table: iptables.FilterTab, args: []string{"-D", ChainForward, "-s", "10.0.0.2", "-p", "tcp", "--sport", "80:81", "-j", "ACCEPT"}},
}
if !reflect.DeepEqual(backend.calls[:4], wantRemovePrefix) {
t.Fatalf("remove transcript changed\ngot %#v\nwant %#v", backend.calls[:4], wantRemovePrefix)
}
}
func TestLegacyNATEnableReplayAndStatusContract(t *testing.T) {
natStatus := strings.Join([]string{
"-N THIRD_PARTY_DNAT",
"-A PREROUTING -j THIRD_PARTY_DNAT",
"-N " + ChainPreRouting,
"-N " + ChainPostRouting,
"-A PREROUTING -j " + ChainPreRouting,
"-A POSTROUTING -j " + ChainPostRouting,
}, "\n")
filterStatus := "-N " + ChainForward + "\n-A FORWARD -j " + ChainForward + "\n-A FORWARD -j DOCKER-USER\n"
backend := &fakeLegacyBackend{stdout: map[string]string{
"nat -S": natStatus,
"filter -S": filterStatus,
}}
system := &fakeForwardingSystem{reads: map[string][]byte{
"/proc/sys/net/ipv4/ip_forward": []byte("1\n"),
"/etc/sysctl.conf": []byte("net.ipv4.tcp_syncookies = 1\n"),
}}
adapter := &legacyNATAdapter{provider: "iptables", backend: backend, system: system}
if err := adapter.Enable(); err != nil {
t.Fatal(err)
}
if len(system.writes) != 2 || system.writes[0].name != "/proc/sys/net/ipv4/ip_forward" ||
!strings.Contains(system.writes[1].data, "net.ipv4.ip_forward = 1") {
t.Fatalf("sysctl writes changed: %#v", system.writes)
}
if !reflect.DeepEqual(system.runs, []commandCall{{name: "sysctl", args: []string{"-p"}}}) {
t.Fatalf("sysctl transcript changed: %#v", system.runs)
}
if init, bind := adapter.InitStatus(); !init || !bind {
t.Fatalf("expected initialized and bound, got %v %v", init, bind)
}
backend.calls = nil
if err := adapter.Replay(); err != nil {
t.Fatal(err)
}
wantLoads := []backendCall{
{method: "load", table: iptables.FilterTab, args: []string{ChainForward, ForwardFile}},
{method: "load", table: iptables.NatTab, args: []string{ChainPreRouting, PreRoutingFile}},
{method: "load", table: iptables.NatTab, args: []string{ChainPostRouting, PostRoutingFile}},
}
if !reflect.DeepEqual(backend.calls, wantLoads) {
t.Fatalf("replay transcript changed: %#v", backend.calls)
}
}
func TestLegacyListParsingContract(t *testing.T) {
stdout := strings.Join([]string{
"1 0 0 DNAT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:10.0.0.2:80",
"2 0 0 REDIRECT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:9000:9001 redir ports 53",
}, "\n")
rules := parseLegacyRules(stdout)
want := []Rule{
{Num: "1", Protocol: "tcp", Port: "8080", TargetIP: "10.0.0.2", TargetPort: "80", Interface: "eth0"},
{Num: "2", Protocol: "udp", Port: "9000-9001", TargetIP: "127.0.0.1", TargetPort: "53", Interface: "*"},
}
if !reflect.DeepEqual(rules, want) {
t.Fatalf("got %#v want %#v", rules, want)
}
}
func assertArgs(t *testing.T, call commandCall, name string, args []string) {
t.Helper()
if call.name != name || !reflect.DeepEqual(call.args, args) {
t.Fatalf("got %#v want %s %#v", call, name, args)
}
}
@@ -0,0 +1,117 @@
package forwarding
import (
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type firewalldCommandRunner interface {
Run(name string, args ...string) error
RunWithStdout(name string, args ...string) (string, error)
}
type defaultFirewalldCommandRunner struct{}
func (defaultFirewalldCommandRunner) Run(name string, args ...string) error {
return cmd.NewCommandMgr().Run(name, args...)
}
func (defaultFirewalldCommandRunner) RunWithStdout(name string, args ...string) (string, error) {
return cmd.NewCommandMgr().RunWithStdout(name, args...)
}
type firewalldAdapter struct {
runner firewalldCommandRunner
}
func newFirewalldAdapter() *firewalldAdapter {
return &firewalldAdapter{runner: defaultFirewalldCommandRunner{}}
}
func (f *firewalldAdapter) Name() string {
return "firewalld"
}
func (f *firewalldAdapter) List() ([]Rule, error) {
if err := f.Enable(); err != nil {
global.LOG.Errorf("init port forward failed, err: %v", err)
}
stdout, err := f.runner.RunWithStdout("firewall-cmd", "--zone=public", "--list-forward-ports")
if err != nil {
return nil, err
}
return parseFirewalldRules(stdout), nil
}
func parseFirewalldRules(stdout string) []Rule {
var rules []Rule
for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimSpace(line)
parts := strings.Split(line, ":")
if len(parts) < 4 {
continue
}
if parts[3] == "toaddr=" {
parts[3] = "127.0.0.1"
}
rules = append(rules, Rule{
Port: strings.TrimPrefix(parts[0], "port="),
Protocol: strings.TrimPrefix(parts[1], "proto="),
TargetIP: strings.TrimPrefix(parts[3], "toaddr="),
TargetPort: strings.TrimPrefix(parts[2], "toport="),
})
}
return rules
}
func (f *firewalldAdapter) Operate(rule Rule, operation string) error {
if cmd.CheckIllegal(operation, rule.Port, rule.Protocol, rule.TargetIP, rule.TargetPort) {
return buserr.New("ErrCmdIllegal")
}
args := buildFirewalldForwardArgs(rule, operation)
if err := f.runner.Run("firewall-cmd", args...); err != nil {
return fmt.Errorf("%s port forward failed, %s", operation, err)
}
return f.reload()
}
func buildFirewalldForwardArgs(rule Rule, operation string) []string {
forwardRule := fmt.Sprintf("--%s-forward-port=port=%s:proto=%s:toport=%s", operation, rule.Port, rule.Protocol, rule.TargetPort)
if rule.TargetIP != "" && rule.TargetIP != "127.0.0.1" && rule.TargetIP != "localhost" {
forwardRule = fmt.Sprintf("--%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s", operation, rule.Port, rule.Protocol, rule.TargetIP, rule.TargetPort)
}
return []string{"--zone=public", forwardRule, "--permanent"}
}
func (f *firewalldAdapter) Enable() error {
stdout, err := f.runner.RunWithStdout("firewall-cmd", "--zone=public", "--query-masquerade")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(stdout), "no") {
if err := f.runner.Run("firewall-cmd", "--zone=public", "--add-masquerade", "--permanent"); err != nil {
return err
}
return f.reload()
}
return err
}
return nil
}
func (f *firewalldAdapter) reload() error {
if err := f.runner.Run("firewall-cmd", "--reload"); err != nil {
return fmt.Errorf("reload firewall failed, err: %v", err)
}
return nil
}
func (f *firewalldAdapter) InitStatus() (bool, bool) {
return true, true
}
func (f *firewalldAdapter) Replay() error {
return nil
}
+328
View File
@@ -0,0 +1,328 @@
package forwarding
import (
"fmt"
"os"
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"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 legacyIptablesBackend interface {
Run(table string, args ...string) error
RunWithStd(table string, args ...string) (string, error)
AddChainWithAppend(table, parentChain, chain string) error
SaveRulesToFile(table, chain, fileName string) error
LoadRulesFromFile(table, chain, fileName string) error
}
type systemIptablesBackend struct{}
func (systemIptablesBackend) Run(table string, args ...string) error {
return iptables.Run(table, args...)
}
func (systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) {
return iptables.RunWithStd(table, args...)
}
func (systemIptablesBackend) AddChainWithAppend(table, parentChain, chain string) error {
return iptables.AddChainWithAppend(table, parentChain, chain)
}
func (systemIptablesBackend) SaveRulesToFile(table, chain, fileName string) error {
return iptables.SaveRulesToFile(table, chain, fileName)
}
func (systemIptablesBackend) LoadRulesFromFile(table, chain, fileName string) error {
return iptables.LoadRulesFromFile(table, chain, fileName)
}
type forwardingSystem interface {
ReadFile(name string) ([]byte, error)
WriteFile(name string, data []byte, perm os.FileMode) error
RunWithOptionalSudo(name string, args ...string) error
}
type defaultForwardingSystem struct{}
func (defaultForwardingSystem) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}
func (defaultForwardingSystem) WriteFile(name string, data []byte, perm os.FileMode) error {
return cmd.WriteFileWithOptionalSudo(name, data, perm)
}
func (defaultForwardingSystem) RunWithOptionalSudo(name string, args ...string) error {
return cmd.NewCommandMgr().RunWithOptionalSudo(name, args...)
}
type legacyNATAdapter struct {
provider string
backend legacyIptablesBackend
system forwardingSystem
}
func newLegacyNATAdapter(provider string) *legacyNATAdapter {
return &legacyNATAdapter{
provider: provider,
backend: systemIptablesBackend{},
system: defaultForwardingSystem{},
}
}
func (l *legacyNATAdapter) Name() string {
return l.provider
}
func (l *legacyNATAdapter) List() ([]Rule, error) {
stdout, err := l.backend.RunWithStd(iptables.NatTab, "-nvL", ChainPreRouting, "--line-numbers")
if err != nil {
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
}
return parseLegacyRules(stdout), nil
}
func (l *legacyNATAdapter) Operate(rule Rule, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
if rule.Protocol == "" || rule.Port == "" || rule.TargetPort == "" {
return fmt.Errorf("protocol, port, and target port are required")
}
var err error
if operation == "add" {
err = l.add(rule)
} else {
err = l.remove(rule)
}
if err != nil {
return err
}
l.persist()
return nil
}
func (l *legacyNATAdapter) add(rule Rule) error {
srcPort := strings.ReplaceAll(rule.Port, "-", ":")
targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":")
if isRemoteTarget(rule.TargetIP) {
args := []string{"-A", ChainPreRouting}
if rule.Interface != "" {
args = append(args, "-i", rule.Interface)
}
args = append(args, "-p", rule.Protocol, "--dport", srcPort, "-j", "DNAT", "--to-destination", rule.TargetIP+":"+rule.TargetPort)
if err := l.backend.Run(iptables.NatTab, args...); err != nil {
return err
}
if err := l.backend.Run(iptables.NatTab, "-A", ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"); err != nil {
return err
}
if err := l.backend.Run(iptables.FilterTab, "-A", ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"); err != nil {
return err
}
return l.backend.Run(iptables.FilterTab, "-A", ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT")
}
args := []string{"-A", ChainPreRouting}
if rule.Interface != "" {
args = append(args, "-i", rule.Interface)
}
args = append(args, "-p", rule.Protocol, "--dport", srcPort, "-j", "REDIRECT", "--to-port", rule.TargetPort)
return l.backend.Run(iptables.NatTab, args...)
}
func (l *legacyNATAdapter) remove(rule Rule) error {
targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":")
if err := l.backend.Run(iptables.NatTab, "-D", ChainPreRouting, rule.Num); err != nil {
return err
}
if !isRemoteTarget(rule.TargetIP) {
return nil
}
if err := l.backend.Run(iptables.NatTab, "-D", ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"); err != nil {
return err
}
if err := l.backend.Run(iptables.FilterTab, "-D", ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"); err != nil {
return err
}
return l.backend.Run(iptables.FilterTab, "-D", ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT")
}
func isRemoteTarget(target string) bool {
return target != "" && target != "127.0.0.1" && target != "localhost"
}
func (l *legacyNATAdapter) persist() {
for _, item := range []struct {
table string
chain string
file string
}{
{iptables.FilterTab, ChainForward, ForwardFile},
{iptables.NatTab, ChainPreRouting, PreRoutingFile},
{iptables.NatTab, ChainPostRouting, PostRoutingFile},
} {
if err := l.backend.SaveRulesToFile(item.table, item.chain, item.file); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", item.chain, err)
}
}
}
func (l *legacyNATAdapter) Enable() error {
if err := l.system.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), constant.FilePerm); err != nil {
return fmt.Errorf("failed to enable IP forwarding: %w", err)
}
if data, err := l.system.ReadFile("/etc/sysctl.conf"); err == nil && !strings.Contains(string(data), "net.ipv4.ip_forward") {
content := strings.TrimRight(string(data), "\n") + "\nnet.ipv4.ip_forward = 1\n"
_ = l.system.WriteFile("/etc/sysctl.conf", []byte(content), constant.FilePerm)
}
_ = l.system.RunWithOptionalSudo("sysctl", "-p")
for _, item := range []struct {
table string
parent string
chain string
}{
{iptables.NatTab, "PREROUTING", ChainPreRouting},
{iptables.NatTab, "POSTROUTING", ChainPostRouting},
{iptables.FilterTab, "FORWARD", ChainForward},
} {
if err := l.backend.AddChainWithAppend(item.table, item.parent, item.chain); err != nil {
return err
}
}
return nil
}
func (l *legacyNATAdapter) InitStatus() (bool, bool) {
data, err := l.system.ReadFile("/proc/sys/net/ipv4/ip_forward")
if err != nil || strings.TrimSpace(string(data)) == "0" {
return false, false
}
natRules, err := l.backend.RunWithStd(iptables.NatTab, "-S")
if err != nil {
return false, false
}
natInit, natBind := checkInitAndBind(
[]string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
[]string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
strings.Split(natRules, "\n"),
)
if !natInit {
return false, false
}
filterRules, err := l.backend.RunWithStd(iptables.FilterTab, "-S")
if err != nil {
return false, false
}
filterInit, filterBind := checkInitAndBind(
[]string{"-N " + ChainForward},
[]string{"-A FORWARD -j " + ChainForward},
strings.Split(filterRules, "\n"),
)
return natInit && filterInit, natBind && filterBind
}
func checkInitAndBind(initRules, bindRules, lines []string) (bool, bool) {
for _, rule := range initRules {
if !containsExactRule(lines, rule) {
return false, false
}
}
for _, rule := range bindRules {
if !containsExactRule(lines, rule) {
return true, false
}
}
return true, true
}
func containsExactRule(lines []string, rule string) bool {
for _, line := range lines {
if strings.TrimSpace(line) == strings.TrimSpace(rule) {
return true
}
}
return false
}
func (l *legacyNATAdapter) Replay() error {
for _, item := range []struct {
table string
chain string
file string
}{
{iptables.FilterTab, ChainForward, ForwardFile},
{iptables.NatTab, ChainPreRouting, PreRoutingFile},
{iptables.NatTab, ChainPostRouting, PostRoutingFile},
} {
if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil {
return err
}
}
return nil
}
func parseLegacyRules(stdout string) []Rule {
var rules []Rule
for _, line := range strings.Split(stdout, "\n") {
fields := strings.Fields(line)
if len(fields) < 13 {
continue
}
rule := Rule{
Num: fields[0],
Protocol: loadProtocol(fields[4]),
Interface: fields[6],
Port: loadSourcePort(fields[11]),
}
if len(fields) == 15 && fields[13] == "ports" {
rule.TargetPort = fields[14]
}
if len(fields) == 13 && strings.HasPrefix(fields[12], "to:") {
parts := strings.Split(fields[12], ":")
if len(parts) > 2 {
rule.TargetPort = parts[2]
rule.TargetIP = parts[1]
}
}
if rule.TargetIP == "" {
rule.TargetIP = "127.0.0.1"
}
rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":")
rules = append(rules, rule)
}
return rules
}
func loadProtocol(protocol string) string {
switch protocol {
case "0":
return "all"
case "1":
return "icmp"
case "6":
return "tcp"
case "17":
return "udp"
default:
return protocol
}
}
func loadSourcePort(value string) string {
port := ""
if strings.Contains(value, "dpt:") {
port = strings.ReplaceAll(value, "dpt:", "")
}
if strings.Contains(value, "dpts:") {
port = strings.ReplaceAll(value, "dpts:", "")
}
return strings.ReplaceAll(port, ":", "-")
}