Skip to content

Commit

Permalink
create a firewall interface (#1850)
Browse files Browse the repository at this point in the history
* create a firewall interface

This is creating a firewall interface and reintroduce the iptables integration as a fallback.

* update

* update

* update

* update

* revert to go v1.20

* use NewFirewallClient name

* formatting

* formatting

---------

Co-authored-by: Roman Dodin <dodin.roman@gmail.com>
  • Loading branch information
steiler and hellt authored Feb 5, 2024
1 parent abf56ab commit f23ddd9
Show file tree
Hide file tree
Showing 8 changed files with 553 additions and 362 deletions.
4 changes: 2 additions & 2 deletions runtime/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func (d *DockerRuntime) postCreateNetActions() (err error) {
if err != nil {
log.Warnf("failed to disable TX checksum offloading for the %s bridge interface: %v", d.mgmt.Bridge, err)
}
err = d.installIPTablesFwdRule()
err = d.installFwdRule()
if err != nil {
log.Warnf("errors during iptables rules install: %v", err)
}
Expand Down Expand Up @@ -368,7 +368,7 @@ func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) {
return err
}

err = d.deleteIPTablesFwdRule()
err = d.deleteFwdRule()
if err != nil {
log.Warnf("errors during iptables rules removal: %v", err)
}
Expand Down
42 changes: 42 additions & 0 deletions runtime/docker/firewall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package docker

import (
log "github.com/sirupsen/logrus"
"github.com/srl-labs/containerlab/runtime/docker/firewall"
)

// deleteFwdRule deletes `allow` rule installed with installFwdRule when the bridge interface doesn't exist anymore.
func (d *DockerRuntime) deleteFwdRule() (err error) {
if !*d.mgmt.ExternalAccess {
return
}

f, err := firewall.NewFirewallClient(d.mgmt.Bridge)
if err != nil {
return err
}

return f.DeleteForwardingRules()
}

// installFwdRule installs the `allow` rule for traffic destined to the nodes
// on the clab management network.
// This rule is required for external access to the nodes.
func (d *DockerRuntime) installFwdRule() (err error) {
if !*d.mgmt.ExternalAccess {
return
}

if d.mgmt.Bridge == "" {
log.Debug("skipping setup of forwarding rules for non-bridged management network")
return
}

f, err := firewall.NewFirewallClient(d.mgmt.Bridge)
if err != nil {
return err
}
log.Debugf("using %s as the firewall interface", f.Name())

return f.InstallForwardingRules()
}
21 changes: 21 additions & 0 deletions runtime/docker/firewall/definitions/definitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package definitions

import "errors"

var ErrNotAvailabel = errors.New("not available")

const (
DockerFWUserChain = "DOCKER-USER"
DockerFWTable = "filter"

IPTablesRuleComment = "set by containerlab"

IPTablesCommentMaxSize = 256
)

// ClabFirewall is the interface that all firewall clients must implement.
type ClabFirewall interface {
DeleteForwardingRules() error
InstallForwardingRules() error
Name() string
}
24 changes: 24 additions & 0 deletions runtime/docker/firewall/firewall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package firewall

import (
"github.com/srl-labs/containerlab/runtime/docker/firewall/definitions"
"github.com/srl-labs/containerlab/runtime/docker/firewall/iptables"
"github.com/srl-labs/containerlab/runtime/docker/firewall/nftables"
)

// NewFirewallClient returns a firewall client based on the availability of nftables or iptables.
func NewFirewallClient(bridgeName string) (definitions.ClabFirewall, error) {
var clf definitions.ClabFirewall

clf, err := nftables.NewNftablesClient(bridgeName)
if err == nil {
return clf, nil
}

clf, err = iptables.NewIpTablesClient(bridgeName)
if err == nil {
return clf, nil
}

return nil, err
}
119 changes: 119 additions & 0 deletions runtime/docker/firewall/iptables/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package iptables

import (
"bytes"
"fmt"
"os/exec"
"strings"

"github.com/google/shlex"
log "github.com/sirupsen/logrus"
"github.com/srl-labs/containerlab/runtime/docker/firewall/definitions"
"github.com/srl-labs/containerlab/utils"
)

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"
)

// IpTablesClient is a client for iptables.
type IpTablesClient struct {
bridgeName string
}

// NewIpTablesClient returns a new IpTablesClient.
func NewIpTablesClient(bridgeName string) (*IpTablesClient, error) {
loaded, err := utils.IsKernelModuleLoaded("ip_tables")
if err != nil {
return nil, err
}

if !loaded {
log.Debug("ip_tables kernel module not available")
// module is not loaded
return nil, definitions.ErrNotAvailabel
}

return &IpTablesClient{
bridgeName: bridgeName,
}, nil
}

// Name returns the name of the firewall client.
func (*IpTablesClient) Name() string {
return ipTables
}

// InstallForwardingRules installs the forwarding rules.
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)
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")
}

cmd, err := shlex.Split(fmt.Sprintf(iptAllowCmd, c.bridgeName))
if err != nil {
return err
}

log.Debugf("Installing iptables rules for bridge %q", c.bridgeName)

stdOutErr, err := exec.Command("iptables", 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)
}

return nil
}

// DeleteForwardingRules deletes the forwarding rules.
func (c *IpTablesClient) DeleteForwardingRules() error {
// first check if a rule exists before trying to delete it
res, err := exec.Command("iptables", strings.Split(iptCheckCmd, " ")...).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
return fmt.Errorf("missing DOCKER-USER iptables chain. See http://containerlab.dev/manual/network/#external-access")
}

if !bytes.Contains(res, []byte(c.bridgeName)) {
log.Debug("external access iptables rule doesn't exist. Skipping deletion")
return nil
}

// we are not deleting the rule if the bridge still exists
// it happens when bridge is either still in use by docker network
// or it is managed externally (created manually)
_, err = utils.BridgeByName(c.bridgeName)
if err == nil {
log.Debugf("bridge %s is still in use, not removing the forwarding rule", c.bridgeName)
return nil
}

cmd, err := shlex.Split(fmt.Sprintf(iptDelCmd, c.bridgeName))
if err != nil {
return err
}

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()
if err != nil {
log.Warnf("Iptables delete stdout/stderr result is: %s", stdOutErr)
return fmt.Errorf("unable to delete iptables rules: %w", err)
}

return nil
}
Loading

0 comments on commit f23ddd9

Please sign in to comment.