Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create a firewall interface #1850

Merged
merged 9 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading