From 99e6ce803c15c8201f39382c3e5a9490a05d0882 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Thu, 16 Jan 2025 12:51:58 +0100 Subject: [PATCH] added iptables based add/delete for v6 --- runtime/docker/firewall/iptables/client.go | 108 ++++++++++++++++----- tests/01-smoke/01-basic-flow.robot | 31 ++++++ 2 files changed, 117 insertions(+), 22 deletions(-) diff --git a/runtime/docker/firewall/iptables/client.go b/runtime/docker/firewall/iptables/client.go index 8af5d29e0..977017525 100644 --- a/runtime/docker/firewall/iptables/client.go +++ b/runtime/docker/firewall/iptables/client.go @@ -13,25 +13,33 @@ import ( ) const ( - iptCheckCmd = "-vL DOCKER-USER" - iptAllowCmd = "-I DOCKER-USER -o %s -j ACCEPT -m comment --comment \"" + definitions.IPTablesRuleComment + "\"" - iptDelCmd = "-D DOCKER-USER -o %s -j ACCEPT -m comment --comment \"" + definitions.IPTablesRuleComment + "\"" - ipTables = "ip_tables" + iptCheckArgs = "-vL DOCKER-USER" + iptAllowArgs = "-I DOCKER-USER -o %s -j ACCEPT -m comment --comment \"" + definitions.IPTablesRuleComment + "\"" + iptDelArgs = "-D DOCKER-USER -o %s -j ACCEPT -m comment --comment \"" + definitions.IPTablesRuleComment + "\"" + ipTables = "ip_tables" + + v4AF = "v4" + ip4tablesCmd = "iptables" + v6AF = "v6" + ip6tablesCmd = "ip6tables" ) // IpTablesClient is a client for iptables. type IpTablesClient struct { bridgeName string + ip6_tables bool } // NewIpTablesClient returns a new IpTablesClient. func NewIpTablesClient(bridgeName string) (*IpTablesClient, error) { - loaded, err := utils.IsKernelModuleLoaded("ip_tables") + v4ModLoaded, err := utils.IsKernelModuleLoaded("ip_tables") if err != nil { return nil, err } - if !loaded { + v6ModLoaded, _ := utils.IsKernelModuleLoaded("ip6_tables") + + if !v4ModLoaded { log.Debug("ip_tables kernel module not available") // module is not loaded return nil, definitions.ErrNotAvailable @@ -39,6 +47,7 @@ func NewIpTablesClient(bridgeName string) (*IpTablesClient, error) { return &IpTablesClient{ bridgeName: bridgeName, + ip6_tables: v6ModLoaded, }, nil } @@ -47,28 +56,40 @@ func (*IpTablesClient) Name() string { return ipTables } -// InstallForwardingRules installs the forwarding rules. +// InstallForwardingRules installs the forwarding rules for v4 and v6 address families. func (c *IpTablesClient) InstallForwardingRules() error { - // first check if a rule already exists to not create duplicates - res, err := exec.Command("iptables", strings.Split(iptCheckCmd, " ")...).Output() - if bytes.Contains(res, []byte(c.bridgeName)) { - log.Debugf("found iptables forwarding rule targeting the bridge %q. Skipping creation of the forwarding rule.", c.bridgeName) + err := c.InstallForwardingRulesForAF(v4AF) + if err != nil { return err } - if err != nil { - // non nil error typically means that DOCKER-USER chain doesn't exist - // this happens with old docker installations (centos7 hello) from default repos - return fmt.Errorf("missing DOCKER-USER iptables chain. See http://containerlab.dev/manual/network/#external-access") + + if c.ip6_tables { + err = c.InstallForwardingRulesForAF(v6AF) } - cmd, err := shlex.Split(fmt.Sprintf(iptAllowCmd, c.bridgeName)) + return err +} + +// InstallForwardingRulesForAF installs the forwarding rules for the specified address family. +func (c *IpTablesClient) InstallForwardingRulesForAF(af string) error { + iptCmd := ip4tablesCmd + if af == v6AF { + iptCmd = ip6tablesCmd + } + + // first check if a rule already exists to not create duplicates + if c.allowRuleForMgmtBrExists(af) { + return nil + } + + cmd, err := shlex.Split(fmt.Sprintf(iptAllowArgs, c.bridgeName)) if err != nil { return err } - log.Debugf("Installing iptables rules for bridge %q", c.bridgeName) + log.Debugf("Installing iptables (%s) rules for bridge %q", af, c.bridgeName) - stdOutErr, err := exec.Command("iptables", cmd...).CombinedOutput() + stdOutErr, err := exec.Command(iptCmd, cmd...).CombinedOutput() if err != nil { log.Warnf("Iptables install stdout/stderr result is: %s", stdOutErr) return fmt.Errorf("unable to install iptables rule using '%s' command: %w", cmd, err) @@ -77,10 +98,29 @@ func (c *IpTablesClient) InstallForwardingRules() error { return nil } -// DeleteForwardingRules deletes the forwarding rules. +// DeleteForwardingRules deletes the forwarding rules for v4 and v6 address families. func (c *IpTablesClient) DeleteForwardingRules() error { + err := c.DeleteForwardingRulesForAF(v4AF) + if err != nil { + return err + } + + if c.ip6_tables { + err = c.InstallForwardingRulesForAF(v6AF) + } + + return err +} + +// DeleteForwardingRulesForAF deletes the forwarding rules for a specified AF. +func (c *IpTablesClient) DeleteForwardingRulesForAF(af string) error { + iptCmd := ip4tablesCmd + if af == v6AF { + iptCmd = ip6tablesCmd + } + // first check if a rule exists before trying to delete it - res, err := exec.Command("iptables", strings.Split(iptCheckCmd, " ")...).Output() + res, err := exec.Command(iptCmd, strings.Split(iptCheckArgs, " ")...).Output() if err != nil { // non nil error typically means that DOCKER-USER chain doesn't exist // this happens with old docker installations (centos7 hello) from default repos @@ -101,7 +141,7 @@ func (c *IpTablesClient) DeleteForwardingRules() error { return nil } - cmd, err := shlex.Split(fmt.Sprintf(iptDelCmd, c.bridgeName)) + cmd, err := shlex.Split(fmt.Sprintf(iptDelArgs, c.bridgeName)) if err != nil { return err } @@ -109,7 +149,7 @@ func (c *IpTablesClient) DeleteForwardingRules() error { log.Debugf("removing clab iptables rules for bridge %q", c.bridgeName) log.Debugf("trying to delete the forwarding rule with cmd: iptables %s", cmd) - stdOutErr, err := exec.Command("iptables", cmd...).CombinedOutput() + stdOutErr, err := exec.Command(iptCmd, cmd...).CombinedOutput() if err != nil { log.Warnf("Iptables delete stdout/stderr result is: %s", stdOutErr) return fmt.Errorf("unable to delete iptables rules: %w", err) @@ -117,3 +157,27 @@ func (c *IpTablesClient) DeleteForwardingRules() error { return nil } + +// allowRuleForMgmtBrExists checks if an allow rule for the provided bridge name exists. +// The actual check doesn't verify that `allow` is set, it just checks if the rule +// has the provided bridge name in the output interface. +func (c *IpTablesClient) allowRuleForMgmtBrExists(af string) bool { + iptCmd := ip4tablesCmd + if af == v6AF { + iptCmd = ip6tablesCmd + } + + res, err := exec.Command(iptCmd, strings.Split(iptCheckArgs, " ")...).CombinedOutput() + if err != nil { + log.Warnf("iptables check error: %s. Output: %s", err, string(res)) + // if we errored on check we don't want to try setting up the rule + return true + } + if bytes.Contains(res, []byte(c.bridgeName)) { + log.Debugf("found iptables forwarding rule targeting the bridge %q. Skipping creation of the forwarding rule.", c.bridgeName) + + return true + } + + return false +} diff --git a/tests/01-smoke/01-basic-flow.robot b/tests/01-smoke/01-basic-flow.robot index 61ac8c263..257655872 100644 --- a/tests/01-smoke/01-basic-flow.robot +++ b/tests/01-smoke/01-basic-flow.robot @@ -317,6 +317,25 @@ Verify iptables allow rule is set ... ignore_case=True ... collapse_spaces=True +Verify ip6tables allow rule is set + [Documentation] Checking if ip6tables allow rule is set so that external traffic can reach containerlab management network + Skip If '${runtime}' != 'docker' + + # Add check for ip6tables availability + ${rc} ${output} = Run And Return Rc And Output which ip6tables + Skip If ${rc} != 0 ip6tables command not found + + + ${ipt} = Run + ... sudo ip6tables -vnL DOCKER-USER + Log ${ipt} + # debian 12 uses `0` for protocol, while previous versions use `all` + Should Contain Any ${ipt} + ... ACCEPT all -- * ${MgmtBr} + ... ACCEPT 0 -- * ${MgmtBr} + ... ignore_case=True + ... collapse_spaces=True + Verify DNS-Server Config [Documentation] Check if the DNS config did take effect Skip If '${runtime}' != 'docker' @@ -403,6 +422,18 @@ Verify iptables allow rule are gone Log ${ipt} Should Not Contain ${ipt} ${MgmtBr} +Verify ip6tables allow rule are gone + [Documentation] Checking if ip6tables allow rule is removed once the lab is destroyed + Skip If '${runtime}' != 'docker' + + # Add check for ip6tables availability + ${rc} ${output} = Run And Return Rc And Output which ip6tables + Skip If ${rc} != 0 ip6tables command not found + + ${ipt} = Run + ... sudo ip6tables -vnL DOCKER-USER + Log ${ipt} + Should Not Contain ${ipt} ${MgmtBr} *** Keywords *** Match IPv6 Address