mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, ":", "-")
|
||||
}
|
||||
Reference in New Issue
Block a user