mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-12 16:00:33 +00:00
Replace database rule synchronization with native rule operations and file-based backup, import, and recovery. Update firewall management views, descriptions, whitelist handling, and translations. Include terminal reconnect fixes and xpack build tags for Darwin build targets. Remove temporary regression test files before committing.
799 lines
23 KiB
Go
799 lines
23 KiB
Go
package forwarding
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
|
"github.com/mattn/go-shellwords"
|
|
)
|
|
|
|
type iptablesBackend interface {
|
|
IPv6Available() bool
|
|
Run(table string, args ...string) error
|
|
RunWithStd(table string, args ...string) (string, error)
|
|
RunIPv6(table string, args ...string) error
|
|
RunIPv6WithStd(table string, args ...string) (string, error)
|
|
Restore(ctx context.Context, family, input string) error
|
|
LoadRulesFromFile(table, chain, fileName string) error
|
|
LoadIPv6RulesFromFile(table, chain, fileName string) error
|
|
}
|
|
|
|
type systemIptablesBackend struct{ ctx context.Context }
|
|
|
|
func (systemIptablesBackend) IPv6Available() bool {
|
|
commands, err := lifecycle.ResolveIptablesCommands()
|
|
return err == nil && commands.IPv6Available()
|
|
}
|
|
|
|
func (b systemIptablesBackend) Run(table string, args ...string) error {
|
|
_, err := iptables_helper.RunWithStdContext(b.ctx, table, args...)
|
|
return err
|
|
}
|
|
|
|
func (b systemIptablesBackend) RunWithStd(table string, args ...string) (string, error) {
|
|
if len(args) == 1 && args[0] == "-S" {
|
|
return iptables_helper.ReadTable(b.ctx, table, false)
|
|
}
|
|
return iptables_helper.RunWithStdContext(b.ctx, table, args...)
|
|
}
|
|
|
|
func (b systemIptablesBackend) RunIPv6(table string, args ...string) error {
|
|
_, err := iptables_helper.RunIPv6WithStdContext(b.ctx, table, args...)
|
|
return err
|
|
}
|
|
|
|
func (b systemIptablesBackend) RunIPv6WithStd(table string, args ...string) (string, error) {
|
|
if len(args) == 1 && args[0] == "-S" {
|
|
return iptables_helper.ReadTable(b.ctx, table, true)
|
|
}
|
|
return iptables_helper.RunIPv6WithStdContext(b.ctx, table, args...)
|
|
}
|
|
|
|
func (systemIptablesBackend) Restore(ctx context.Context, family, input string) error {
|
|
commands, err := lifecycle.ResolveIptablesCommands()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
executable := commands.Restore4
|
|
if family == FamilyIPv6 {
|
|
executable = commands.Restore6
|
|
if executable == "" {
|
|
return fmt.Errorf("ip6tables-restore command family is unavailable")
|
|
}
|
|
}
|
|
var stderr strings.Builder
|
|
manager := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(60*time.Second), cmd.WithStdin(strings.NewReader(input)), cmd.WithStderr(&stderr))
|
|
err = manager.RunWithOptionalSudo(executable, "--noflush", "--wait")
|
|
if err == nil && strings.TrimSpace(stderr.String()) != "" {
|
|
err = fmt.Errorf("firewall command warning: %s", strings.TrimSpace(stderr.String()))
|
|
}
|
|
return errors.Join(firewallutil.WrapBatchCommandError(executable+" --noflush --wait", input, err), ctx.Err())
|
|
}
|
|
|
|
func (systemIptablesBackend) LoadRulesFromFile(table, chain, fileName string) error {
|
|
return iptables_helper.LoadRulesFromFile(table, chain, fileName)
|
|
}
|
|
|
|
func (systemIptablesBackend) LoadIPv6RulesFromFile(table, chain, fileName string) error {
|
|
return iptables_helper.LoadIPv6RulesFromFile(table, chain, fileName)
|
|
}
|
|
|
|
type forwardingSystem interface {
|
|
ReadFile(name string) ([]byte, error)
|
|
WriteFile(name string, data []byte, perm os.FileMode) 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)
|
|
}
|
|
|
|
type Iptables struct {
|
|
ctx context.Context
|
|
provider string
|
|
backend iptablesBackend
|
|
system forwardingSystem
|
|
}
|
|
|
|
func NewIptables(ctx context.Context, provider string) *Iptables {
|
|
return &Iptables{
|
|
ctx: ctx,
|
|
provider: provider,
|
|
backend: systemIptablesBackend{ctx: ctx},
|
|
system: defaultForwardingSystem{},
|
|
}
|
|
}
|
|
|
|
func (l *Iptables) Name() string {
|
|
return l.provider
|
|
}
|
|
|
|
func (l *Iptables) List() ([]Rule, error) {
|
|
stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
|
|
}
|
|
rules := parseIptablesRules(stdout, FamilyIPv4)
|
|
if !l.backend.IPv6Available() {
|
|
return rules, nil
|
|
}
|
|
stdout, err = l.backend.RunIPv6WithStd(iptables_helper.NatTab, "-S")
|
|
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
return rules, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err)
|
|
}
|
|
return append(rules, parseIptablesRules(stdout, FamilyIPv6)...), nil
|
|
}
|
|
|
|
func (l *Iptables) ReplaceRules(rules []Rule) error {
|
|
byFamily := map[string][]Rule{
|
|
FamilyIPv4: nil,
|
|
FamilyIPv6: nil,
|
|
}
|
|
for _, rule := range rules {
|
|
normalized, err := NormalizeRule(rule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized)
|
|
}
|
|
var failures []error
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
if len(byFamily[family]) > 0 {
|
|
failures = append(failures, fmt.Errorf("ip6tables command family is unavailable"))
|
|
}
|
|
continue
|
|
}
|
|
initialized, _, err := l.FamilyStatus(family)
|
|
if family == FamilyIPv6 && len(byFamily[family]) == 0 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
failures = append(failures, err)
|
|
continue
|
|
}
|
|
if !initialized {
|
|
if len(byFamily[family]) > 0 {
|
|
failures = append(failures, fmt.Errorf("%s forwarding chains are not initialized", family))
|
|
}
|
|
continue
|
|
}
|
|
script, err := buildIptablesForwardScript(byFamily[family], OperationAdd, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := l.backend.Restore(l.ctx, family, script); err != nil {
|
|
return fmt.Errorf("restore %s forwarding rules: %w", family, err)
|
|
}
|
|
}
|
|
return errors.Join(failures...)
|
|
}
|
|
|
|
func (l *Iptables) CreateRules(ctx context.Context, rules []Rule) error {
|
|
if len(rules) == 0 {
|
|
return nil
|
|
}
|
|
script, err := buildIptablesForwardScript(rules, OperationAdd, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
family := rules[0].Family
|
|
if family == "" {
|
|
family = FamilyIPv4
|
|
}
|
|
return l.backend.Restore(ctx, family, script)
|
|
}
|
|
|
|
func (l *Iptables) DeleteRules(ctx context.Context, rules []Rule) error {
|
|
if len(rules) == 0 {
|
|
return nil
|
|
}
|
|
script, err := buildIptablesForwardScript(rules, OperationRemove, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
family := rules[0].Family
|
|
if family == "" {
|
|
family = FamilyIPv4
|
|
}
|
|
return l.backend.Restore(ctx, family, script)
|
|
}
|
|
|
|
func buildIptablesForwardScript(rules []Rule, operation OperationType, replace bool) (string, error) {
|
|
var natRules, filterRules [][]string
|
|
if replace {
|
|
natRules = [][]string{{"-F", ChainPreRouting}, {"-F", ChainPostRouting}}
|
|
filterRules = [][]string{{"-F", ChainForward}}
|
|
}
|
|
verb := "-A"
|
|
if operation == OperationRemove {
|
|
verb = "-D"
|
|
} else if operation != OperationAdd {
|
|
return "", fmt.Errorf("unsupported forwarding operation %q", operation)
|
|
}
|
|
family := ""
|
|
for _, rule := range rules {
|
|
normalized, err := NormalizeRule(rule)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rule = normalized
|
|
if family != "" && family != rule.Family {
|
|
return "", fmt.Errorf("iptables forwarding batch must use one address family")
|
|
}
|
|
family = rule.Family
|
|
sourcePort := strings.ReplaceAll(rule.Port, "-", ":")
|
|
targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":")
|
|
preRouting := []string{verb, ChainPreRouting}
|
|
if rule.Interface != "" {
|
|
preRouting = append(preRouting, "-i", rule.Interface)
|
|
}
|
|
preRouting = append(preRouting, "-p", rule.Protocol, "--dport", sourcePort)
|
|
if !isRemoteTarget(rule.Family, rule.TargetIP) {
|
|
natRules = append(natRules, append(preRouting, "-j", "REDIRECT", "--to-port", rule.TargetPort))
|
|
continue
|
|
}
|
|
natRules = append(natRules,
|
|
append(preRouting, "-j", "DNAT", "--to-destination", forwardingTarget(rule)),
|
|
[]string{verb, ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"},
|
|
)
|
|
filterRules = append(filterRules,
|
|
[]string{verb, ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"},
|
|
[]string{verb, ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"},
|
|
)
|
|
}
|
|
var script strings.Builder
|
|
for _, table := range []struct {
|
|
name string
|
|
rules [][]string
|
|
}{{iptables_helper.NatTab, natRules}, {iptables_helper.FilterTab, filterRules}} {
|
|
script.WriteByte('*')
|
|
script.WriteString(table.name)
|
|
script.WriteByte('\n')
|
|
for _, rule := range table.rules {
|
|
for index, token := range rule {
|
|
if token == "" || strings.ContainsAny(token, " \t\r\n\"'") {
|
|
return "", fmt.Errorf("invalid iptables-restore token %q", token)
|
|
}
|
|
if index > 0 {
|
|
script.WriteByte(' ')
|
|
}
|
|
script.WriteString(token)
|
|
}
|
|
script.WriteByte('\n')
|
|
}
|
|
script.WriteString("COMMIT\n")
|
|
}
|
|
return script.String(), nil
|
|
}
|
|
|
|
func forwardingTarget(rule Rule) string {
|
|
if rule.Family == FamilyIPv6 {
|
|
return "[" + rule.TargetIP + "]:" + rule.TargetPort
|
|
}
|
|
return rule.TargetIP + ":" + rule.TargetPort
|
|
}
|
|
|
|
func isRemoteTarget(family, target string) bool {
|
|
if family == FamilyIPv6 {
|
|
return target != "" && target != "::1" && target != "localhost"
|
|
}
|
|
return target != "" && target != "127.0.0.1" && target != "localhost"
|
|
}
|
|
|
|
func (l *Iptables) Enable(families ...string) error {
|
|
if err := l.OperateFamily(FamilyIPv4, true); err != nil {
|
|
return err
|
|
}
|
|
if len(families) > 0 && !slices.Contains(families, FamilyIPv6) {
|
|
return nil
|
|
}
|
|
initialized, _, err := l.FamilyStatus(FamilyIPv6)
|
|
if err != nil || !initialized {
|
|
return err
|
|
}
|
|
return l.OperateFamily(FamilyIPv6, false)
|
|
}
|
|
|
|
func (l *Iptables) batchEnsureChains(family string, initialize bool) error {
|
|
list := l.backend.RunWithStd
|
|
if family == FamilyIPv6 {
|
|
list = l.backend.RunIPv6WithStd
|
|
}
|
|
outputs := make(map[string]string, 2)
|
|
for _, table := range []string{iptables_helper.NatTab, iptables_helper.FilterTab} {
|
|
output, err := list(table, "-S")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outputs[table] = output
|
|
}
|
|
if !initialize && (!containsExactLine(outputs[iptables_helper.NatTab], "-N "+ChainPreRouting) ||
|
|
!containsExactLine(outputs[iptables_helper.NatTab], "-N "+ChainPostRouting) ||
|
|
!containsExactLine(outputs[iptables_helper.FilterTab], "-N "+ChainForward)) {
|
|
return fmt.Errorf("%s forwarding chains are not initialized", family)
|
|
}
|
|
script := buildIptablesForwardLifecycleScript(outputs, true)
|
|
if script == "" {
|
|
return nil
|
|
}
|
|
if err := l.backend.Restore(l.ctx, family, script); err != nil {
|
|
return fmt.Errorf("batch initialize %s forwarding chains: %w", family, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *Iptables) Cleanup() error {
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
continue
|
|
}
|
|
list := l.backend.RunWithStd
|
|
if family == FamilyIPv6 {
|
|
list = l.backend.RunIPv6WithStd
|
|
}
|
|
outputs := make(map[string]string, 2)
|
|
for _, table := range []string{iptables_helper.NatTab, iptables_helper.FilterTab} {
|
|
rules, err := list(table, "-S")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outputs[table] = rules
|
|
}
|
|
script := buildIptablesForwardLifecycleScript(outputs, false)
|
|
if script != "" {
|
|
if err := l.backend.Restore(l.ctx, family, script); err != nil {
|
|
return fmt.Errorf("batch delete %s forwarding chains: %w", family, err)
|
|
}
|
|
}
|
|
}
|
|
for _, file := range []string{ForwardFile, PreRoutingFile, PostRoutingFile,
|
|
iptables_helper.IPv6FileName(ForwardFile), iptables_helper.IPv6FileName(PreRoutingFile), iptables_helper.IPv6FileName(PostRoutingFile)} {
|
|
if err := os.Remove(filepath.Join(global.Dir.FirewallDir, file)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) string {
|
|
items := []struct{ table, parent, chain string }{
|
|
{iptables_helper.NatTab, "PREROUTING", ChainPreRouting},
|
|
{iptables_helper.NatTab, "POSTROUTING", ChainPostRouting},
|
|
}
|
|
byTable := make(map[string][]string, 2)
|
|
for _, item := range items {
|
|
output := outputs[item.table]
|
|
chainExists := containsExactLine(output, "-N "+item.chain)
|
|
binding := "-A " + item.parent + " -j " + item.chain
|
|
bindingCount := countExactLines(output, binding)
|
|
if create {
|
|
if !chainExists {
|
|
byTable[item.table] = append(byTable[item.table], "-N "+item.chain)
|
|
}
|
|
if bindingCount == 0 {
|
|
byTable[item.table] = append(byTable[item.table], "-A "+item.parent+" -j "+item.chain)
|
|
}
|
|
continue
|
|
}
|
|
for range bindingCount {
|
|
byTable[item.table] = append(byTable[item.table], "-D "+item.parent+" -j "+item.chain)
|
|
}
|
|
if chainExists {
|
|
byTable[item.table] = append(byTable[item.table], "-F "+item.chain, "-X "+item.chain)
|
|
}
|
|
}
|
|
|
|
filterOutput := outputs[iptables_helper.FilterTab]
|
|
filterChainExists := containsExactLine(filterOutput, "-N "+ChainForward)
|
|
filterBinding := "-A FORWARD -j " + ChainForward
|
|
filterBindingCount := countExactLines(filterOutput, filterBinding)
|
|
if create {
|
|
if !filterChainExists {
|
|
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+ChainForward)
|
|
}
|
|
if !forwardBindingEffective(filterOutput) {
|
|
for range filterBindingCount {
|
|
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward)
|
|
}
|
|
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], canonicalForwardBindingRule(filterOutput))
|
|
}
|
|
} else {
|
|
for range filterBindingCount {
|
|
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward)
|
|
}
|
|
if filterChainExists {
|
|
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+ChainForward, "-X "+ChainForward)
|
|
}
|
|
}
|
|
var script strings.Builder
|
|
for _, table := range []string{iptables_helper.NatTab, iptables_helper.FilterTab} {
|
|
lines := byTable[table]
|
|
if len(lines) == 0 {
|
|
continue
|
|
}
|
|
script.WriteByte('*')
|
|
script.WriteString(table)
|
|
script.WriteByte('\n')
|
|
script.WriteString(strings.Join(lines, "\n"))
|
|
script.WriteString("\nCOMMIT\n")
|
|
}
|
|
return script.String()
|
|
}
|
|
|
|
func countExactLines(output, want string) int {
|
|
count := 0
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if strings.TrimSpace(line) == want {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func forwardBindingEffective(output string) bool {
|
|
binding := "-A FORWARD -j " + ChainForward
|
|
bindingPosition := 0
|
|
terminalPosition := 0
|
|
position := 0
|
|
bindings := 0
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.HasPrefix(line, "-A FORWARD ") {
|
|
continue
|
|
}
|
|
position++
|
|
if line == binding {
|
|
bindings++
|
|
bindingPosition = position
|
|
}
|
|
if terminalPosition == 0 && isUnconditionalForwardTerminal(line) {
|
|
terminalPosition = position
|
|
}
|
|
}
|
|
return bindings == 1 && (terminalPosition == 0 || bindingPosition < terminalPosition)
|
|
}
|
|
|
|
func canonicalForwardBindingRule(output string) string {
|
|
binding := "-A FORWARD -j " + ChainForward
|
|
position := 1
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.HasPrefix(line, "-A FORWARD ") || line == binding {
|
|
continue
|
|
}
|
|
if isUnconditionalForwardTerminal(line) {
|
|
return fmt.Sprintf("-I FORWARD %d -j %s", position, ChainForward)
|
|
}
|
|
position++
|
|
}
|
|
return "-A FORWARD -j " + ChainForward
|
|
}
|
|
|
|
func isUnconditionalForwardTerminal(line string) bool {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 || fields[0] != "-A" || fields[1] != "FORWARD" || fields[2] != "-j" {
|
|
return false
|
|
}
|
|
switch fields[3] {
|
|
case "ACCEPT", "DROP", "REJECT", "RETURN":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func containsExactLine(output, want string) bool {
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if strings.TrimSpace(line) == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (l *Iptables) InitStatus() (bool, bool, error) {
|
|
ipv4Init, ipv4Bind, err := l.familyInitStatus(FamilyIPv4)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
if !l.backend.IPv6Available() {
|
|
return ipv4Init, ipv4Bind, nil
|
|
}
|
|
ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6)
|
|
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
return ipv4Init, ipv4Bind, nil
|
|
}
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
return ipv4Init || ipv6Init, ipv4Bind || ipv6Bind, nil
|
|
}
|
|
|
|
func (l *Iptables) familyInitStatus(family string) (bool, bool, error) {
|
|
initialized, bound, _, err := l.FamilyState(family)
|
|
return initialized, bound, err
|
|
}
|
|
|
|
func (l *Iptables) FamilyState(family string) (bool, bool, bool, error) {
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
return false, false, false, nil
|
|
}
|
|
sysctlPath := "/proc/sys/net/ipv4/ip_forward"
|
|
label := "IPv4"
|
|
list := l.backend.RunWithStd
|
|
if family == FamilyIPv6 {
|
|
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
|
|
label = "IPv6"
|
|
list = l.backend.RunIPv6WithStd
|
|
}
|
|
data, err := l.system.ReadFile(sysctlPath)
|
|
if family == FamilyIPv6 && errors.Is(err, os.ErrNotExist) {
|
|
return false, false, false, nil
|
|
}
|
|
if err != nil {
|
|
return false, false, false, fmt.Errorf("read %s forwarding status: %w", label, err)
|
|
}
|
|
forwardingEnabled := strings.TrimSpace(string(data)) != "0"
|
|
natRules, err := list(iptables_helper.NatTab, "-S")
|
|
if err != nil {
|
|
return false, false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err)
|
|
}
|
|
natInit, natBind := checkInitAndBind(
|
|
[]string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
|
|
[]string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
|
|
strings.Split(natRules, "\n"),
|
|
)
|
|
filterRules, err := list(iptables_helper.FilterTab, "-S")
|
|
if err != nil {
|
|
return false, false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err)
|
|
}
|
|
filterInit, _ := checkInitAndBind(
|
|
[]string{"-N " + ChainForward},
|
|
nil,
|
|
strings.Split(filterRules, "\n"),
|
|
)
|
|
filterBind := forwardBindingEffective(filterRules)
|
|
initialized := natInit && filterInit
|
|
present := containsExactLine(natRules, "-N "+ChainPreRouting) || containsExactLine(natRules, "-N "+ChainPostRouting) || containsExactLine(filterRules, "-N "+ChainForward)
|
|
return initialized, forwardingEnabled && natBind && filterInit && filterBind, present && !initialized, nil
|
|
}
|
|
|
|
func (l *Iptables) FamilyStatus(family string) (bool, bool, error) {
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
return false, false, nil
|
|
}
|
|
return l.familyInitStatus(family)
|
|
}
|
|
|
|
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 *Iptables) Replay() error {
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
continue
|
|
}
|
|
if err := l.batchEnsureChains(family, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, item := range []struct {
|
|
table string
|
|
chain string
|
|
file string
|
|
}{
|
|
{iptables_helper.FilterTab, ChainForward, ForwardFile},
|
|
{iptables_helper.NatTab, ChainPreRouting, PreRoutingFile},
|
|
{iptables_helper.NatTab, ChainPostRouting, PostRoutingFile},
|
|
} {
|
|
if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil {
|
|
return err
|
|
}
|
|
if l.backend.IPv6Available() {
|
|
if err := l.backend.LoadIPv6RulesFromFile(item.table, item.chain, iptables_helper.IPv6FileName(item.file)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseIptablesRules(stdout, family string) []Rule {
|
|
var rules []Rule
|
|
num := 0
|
|
lines:
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
fields, err := shellwords.Parse(line)
|
|
if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != ChainPreRouting {
|
|
continue
|
|
}
|
|
num++
|
|
rule := Rule{Num: strconv.Itoa(num), Family: family}
|
|
target := ""
|
|
for index := 2; index < len(fields); index++ {
|
|
var value *string
|
|
switch fields[index] {
|
|
case "!":
|
|
continue lines
|
|
case "-p", "--protocol":
|
|
value = &rule.Protocol
|
|
case "-i", "--in-interface":
|
|
value = &rule.Interface
|
|
case "--dport", "--destination-port":
|
|
value = &rule.Port
|
|
case "-j", "--jump":
|
|
value = &target
|
|
case "--to-destination":
|
|
if index+1 >= len(fields) {
|
|
continue lines
|
|
}
|
|
index++
|
|
rule.TargetIP, rule.TargetPort = parseIptablesTarget(fields[index])
|
|
case "--to-ports", "--to-port":
|
|
value = &rule.TargetPort
|
|
case "-m", "--match", "--comment":
|
|
if index+1 >= len(fields) {
|
|
continue lines
|
|
}
|
|
index++
|
|
}
|
|
if value != nil {
|
|
if index+1 >= len(fields) {
|
|
continue lines
|
|
}
|
|
index++
|
|
*value = fields[index]
|
|
}
|
|
}
|
|
if rule.Protocol == "" || rule.Port == "" || rule.TargetPort == "" {
|
|
continue
|
|
}
|
|
switch target {
|
|
case "REDIRECT":
|
|
rule.TargetIP = "127.0.0.1"
|
|
if family == FamilyIPv6 {
|
|
rule.TargetIP = "::1"
|
|
}
|
|
case "DNAT":
|
|
if rule.TargetIP == "" {
|
|
continue
|
|
}
|
|
default:
|
|
continue
|
|
}
|
|
rule.Protocol = loadProtocol(rule.Protocol)
|
|
rule.Port = strings.ReplaceAll(rule.Port, ":", "-")
|
|
rule.TargetPort = strings.ReplaceAll(rule.TargetPort, ":", "-")
|
|
rules = append(rules, rule)
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func parseIptablesTarget(value string) (string, string) {
|
|
if strings.HasPrefix(value, "[") {
|
|
separator := strings.LastIndex(value, "]:")
|
|
if separator > 0 {
|
|
return value[1:separator], value[separator+2:]
|
|
}
|
|
return "", ""
|
|
}
|
|
separator := strings.LastIndex(value, ":")
|
|
if separator <= 0 {
|
|
return "", ""
|
|
}
|
|
return value[:separator], value[separator+1:]
|
|
}
|
|
|
|
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 (l *Iptables) OperateFamily(family string, initialize bool) error {
|
|
if family != FamilyIPv4 && family != FamilyIPv6 {
|
|
return fmt.Errorf("unsupported forwarding family %q", family)
|
|
}
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
return fmt.Errorf("ip6tables command family is unavailable")
|
|
}
|
|
if !initialize {
|
|
initialized, _, err := l.FamilyStatus(family)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !initialized {
|
|
return fmt.Errorf("%s forwarding chains are not initialized", family)
|
|
}
|
|
}
|
|
if err := ensureFamilyForwardingSysctls(l.system, family); err != nil {
|
|
return err
|
|
}
|
|
return l.batchEnsureChains(family, initialize)
|
|
}
|
|
|
|
func (l *Iptables) UnbindFamily(family string) error {
|
|
if family != FamilyIPv4 && family != FamilyIPv6 {
|
|
return fmt.Errorf("unsupported forwarding family %q", family)
|
|
}
|
|
if family == FamilyIPv6 && !l.backend.IPv6Available() {
|
|
return nil
|
|
}
|
|
read := l.backend.RunWithStd
|
|
if family == FamilyIPv6 {
|
|
read = l.backend.RunIPv6WithStd
|
|
}
|
|
var script strings.Builder
|
|
for _, table := range []string{iptables_helper.NatTab, iptables_helper.FilterTab} {
|
|
output, err := read(table, "-S")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var removals []string
|
|
for _, pair := range [][2]string{{"PREROUTING", ChainPreRouting}, {"POSTROUTING", ChainPostRouting}, {"FORWARD", ChainForward}} {
|
|
rule := "-A " + pair[0] + " -j " + pair[1]
|
|
for range countExactLines(output, rule) {
|
|
removals = append(removals, "-D "+pair[0]+" -j "+pair[1])
|
|
}
|
|
}
|
|
if len(removals) > 0 {
|
|
script.WriteString("*" + table + "\n" + strings.Join(removals, "\n") + "\nCOMMIT\n")
|
|
}
|
|
}
|
|
if script.Len() == 0 {
|
|
return nil
|
|
}
|
|
return l.backend.Restore(l.ctx, family, script.String())
|
|
}
|