mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-21 00:24:12 +08:00
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.
275 lines
10 KiB
Go
275 lines
10 KiB
Go
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)
|
|
}
|
|
}
|