diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 1b6296c14..86850f123 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -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) } @@ -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) } diff --git a/runtime/docker/firewall.go b/runtime/docker/firewall.go new file mode 100644 index 000000000..e5a60a676 --- /dev/null +++ b/runtime/docker/firewall.go @@ -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() +} diff --git a/runtime/docker/firewall/definitions/definitions.go b/runtime/docker/firewall/definitions/definitions.go new file mode 100644 index 000000000..c83d44235 --- /dev/null +++ b/runtime/docker/firewall/definitions/definitions.go @@ -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 +} diff --git a/runtime/docker/firewall/firewall.go b/runtime/docker/firewall/firewall.go new file mode 100644 index 000000000..be77d1c13 --- /dev/null +++ b/runtime/docker/firewall/firewall.go @@ -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 +} diff --git a/runtime/docker/firewall/iptables/client.go b/runtime/docker/firewall/iptables/client.go new file mode 100644 index 000000000..cf5d78685 --- /dev/null +++ b/runtime/docker/firewall/iptables/client.go @@ -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 +} diff --git a/runtime/docker/firewall/nftables/client.go b/runtime/docker/firewall/nftables/client.go new file mode 100644 index 000000000..fb6926de4 --- /dev/null +++ b/runtime/docker/firewall/nftables/client.go @@ -0,0 +1,265 @@ +package nftables + +import ( + "bytes" + "errors" + "fmt" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/google/nftables/xt" + log "github.com/sirupsen/logrus" + "github.com/srl-labs/containerlab/runtime/docker/firewall/definitions" + "github.com/srl-labs/containerlab/utils" +) + +const nfTables = "nf_tables" + +// NftablesClient is a client for nftables. +type NftablesClient struct { + nftConn *nftables.Conn + bridgeName string +} + +// NewNftablesClient returns a new NftablesClient. +func NewNftablesClient(bridgeName string) (*NftablesClient, error) { + loaded, err := utils.IsKernelModuleLoaded("nf_tables") + if err != nil { + return nil, err + } + + if !loaded { + log.Debug("nf_tables kernel module not available") + // module is not loaded + return nil, definitions.ErrNotAvailabel + } + + // setup netlink connection with nftables + nftConn, err := nftables.New(nftables.AsLasting()) + if err != nil { + return nil, err + } + + nftC := &NftablesClient{ + nftConn: nftConn, + bridgeName: bridgeName, + } + + return nftC, nil +} + +// Name returns the name of the firewall client. +func (*NftablesClient) Name() string { + return nfTables +} + +// DeleteForwardingRules deletes the forwarding rules. +func (c *NftablesClient) DeleteForwardingRules() error { + if c.bridgeName == "docker0" { + log.Debug("skipping deletion of iptables forwarding rule for non-bridged or default management network") + return nil + } + + // first check if a rule already exists to not create duplicates + defer c.close() + + rules, err := c.getRules(definitions.DockerFWUserChain, definitions.DockerFWTable, nftables.TableFamilyIPv4) + if err != nil { + return fmt.Errorf("%w. See http://containerlab.dev/manual/network/#external-access", err) + } + + mgmtBrRules := c.getRulesForMgmtBr(c.bridgeName, rules) + if len(mgmtBrRules) == 0 { + 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 + } + + log.Debugf("removing clab iptables rules for bridge %q", c.bridgeName) + for _, r := range mgmtBrRules { + c.deleteRule(r) + } + + c.flush() + return nil +} + +// InstallForwardingRules installs the forwarding rules. +func (c *NftablesClient) InstallForwardingRules() error { + + defer c.close() + + rules, err := c.getRules(definitions.DockerFWUserChain, definitions.DockerFWTable, nftables.TableFamilyIPv4) + if err != nil { + return fmt.Errorf("%w. See http://containerlab.dev/manual/network/#external-access", err) + } + + if c.allowRuleForMgmtBrExists(c.bridgeName, rules) { + log.Debugf("found iptables forwarding rule targeting the bridge %q. Skipping creation of the forwarding rule.", c.bridgeName) + return nil + } + + log.Debugf("Installing iptables rules for bridge %q", c.bridgeName) + + // create a new rule + rule, err := c.newClabNftablesRule(definitions.DockerFWUserChain, definitions.DockerFWTable, nftables.TableFamilyIPv4, 0) + if err != nil { + return err + } + // set Output interface match + rule.AddOutputInterfaceFilter(c.bridgeName) + + // add a comment + err = rule.AddComment(definitions.IPTablesRuleComment) + if err != nil { + return err + } + // add a counter + err = rule.AddCounter() + if err != nil { + return err + } + // make it an ACCEPT rule + err = rule.AddVerdictAccept() + if err != nil { + return err + } + // mark and note for installation + c.insertRule(rule.rule) + // flush changes out to nftables + c.flush() + + return nil +} + +func (nftC *NftablesClient) getChains(name string) ([]*nftables.Chain, error) { + var result []*nftables.Chain + + chains, err := nftC.nftConn.ListChains() + if err != nil { + return nil, err + } + + for _, c := range chains { + if c.Name == name { + result = append(result, c) + } + } + return result, nil +} + +func (nftC *NftablesClient) deleteRule(r *nftables.Rule) { + nftC.nftConn.DelRule(r) +} + +// getRules returns all rules for the provided chain name, table name and family. +func (nftC *NftablesClient) getRules(chainName, tableName string, family nftables.TableFamily) ([]*nftables.Rule, error) { + // get chain reference + chains, err := nftC.getChains(chainName) + if err != nil { + return nil, err + } + + for _, c := range chains { + if c.Table.Name == tableName && c.Table.Family == family { + return nftC.nftConn.GetRules(c.Table, c) + } + } + + return nil, fmt.Errorf("no match for chain %q, table %q with family %q found", chainName, tableName, family) +} + +func (nftC *NftablesClient) newClabNftablesRule(chainName, tableName string, family nftables.TableFamily, position uint64) (*clabNftablesRule, error) { + chains, err := nftC.getChains(chainName) + if err != nil { + return nil, err + } + + for _, c := range chains { + if c.Table.Name == tableName && c.Table.Family == family { + r := &nftables.Rule{ + Handle: 0, + Position: position, + Table: c.Table, + Chain: c, + Exprs: []expr.Any{}, + } + + return &clabNftablesRule{rule: r}, nil + } + } + + return nil, errors.New("chain " + chainName + " not found in table " + tableName + " with family " + string(family)) +} + +// InsertRule inserts a rule. +func (nftC *NftablesClient) insertRule(r *nftables.Rule) { + nftC.nftConn.InsertRule(r) +} + +// Flush sends all buffered commands in a single batch to nftables. +func (nftC *NftablesClient) flush() error { + return nftC.nftConn.Flush() +} + +// Close closes connection to nftables. +func (nftC *NftablesClient) close() { + nftC.nftConn.CloseLasting() +} + +// 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 match and has a comment that is setup +// by containerlab. +func (nftC *NftablesClient) allowRuleForMgmtBrExists(brName string, rules []*nftables.Rule) bool { + return len(nftC.getRulesForMgmtBr(brName, rules)) > 0 +} + +// getRulesForMgmtBr returns all rules that have the provided bridge name in the output interface match +// and have a comment that is setup by containerlab. +func (*NftablesClient) getRulesForMgmtBr(brName string, rules []*nftables.Rule) []*nftables.Rule { + var result []*nftables.Rule + + for _, r := range rules { + oifNameFound := false + commentFound := false + + for _, e := range r.Exprs { + switch v := e.(type) { + // Cmp is a comparison expression + // in the case of the rule we are looking for, it should be a comparison of the output interface name + case *expr.Cmp: + if bytes.Equal(v.Data, []byte(brName+"\x00")) { + oifNameFound = true + } + // Match is a match expression + // in the case of the rule we are looking for, it should contain + // a comment extension with the comment set by containerlab + case *expr.Match: + if v.Name == "comment" { + if val, ok := v.Info.(*xt.Unknown); ok { + if bytes.HasPrefix(*val, []byte(definitions.IPTablesRuleComment)) { + commentFound = true + } + } + } + default: + continue + } + } + + if oifNameFound && commentFound { + result = append(result, r) + } + } + + return result +} diff --git a/runtime/docker/firewall/nftables/rule.go b/runtime/docker/firewall/nftables/rule.go new file mode 100644 index 000000000..1e2ad43d1 --- /dev/null +++ b/runtime/docker/firewall/nftables/rule.go @@ -0,0 +1,80 @@ +package nftables + +import ( + "fmt" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/google/nftables/xt" + "github.com/srl-labs/containerlab/runtime/docker/firewall/definitions" +) + +type clabNftablesRule struct { + rule *nftables.Rule +} + +func (cnr *clabNftablesRule) AddOutputInterfaceFilter(oif string) { + // define the metadata to evaluate + metaOifName := &expr.Meta{ + Key: expr.MetaKeyOIFNAME, + SourceRegister: false, + Register: 1, + } + // define the comparison + comp := &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte(oif + "\x00"), + } + + // add expr to rule + cnr.rule.Exprs = append(cnr.rule.Exprs, metaOifName, comp) +} + +func (cnr *clabNftablesRule) AddCounter() error { + c := &expr.Counter{} + cnr.rule.Exprs = append(cnr.rule.Exprs, c) + return nil +} + +func (cnr *clabNftablesRule) AddVerdictAccept() error { + v := &expr.Verdict{ + Kind: expr.VerdictAccept, + } + cnr.rule.Exprs = append(cnr.rule.Exprs, v) + return nil +} + +func (cnr *clabNftablesRule) AddVerdictDrop() error { + v := &expr.Verdict{ + Kind: expr.VerdictDrop, + } + cnr.rule.Exprs = append(cnr.rule.Exprs, v) + return nil +} + +// AddComment adds a comment to the rule. +func (cnr *clabNftablesRule) AddComment(comment string) error { + // convert comment to byte + actualCommentByte := []byte(comment) + // check comment length not exceded + if len(actualCommentByte) > definitions.IPTablesCommentMaxSize { + return fmt.Errorf("comment max length is %d you've provided %d bytes", definitions.IPTablesCommentMaxSize, len(actualCommentByte)) + } + + // copy into byte alice of XT_MAX_COMMENT_LEN length + commentBytes := make([]byte, definitions.IPTablesCommentMaxSize) + copy(commentBytes, actualCommentByte) + + // create extension Info parameter as Unknown extension + commentXTInfo := xt.Unknown(commentBytes) + + // create the extension + e := &expr.Match{ + Name: "comment", + Info: &commentXTInfo, + } + // add extension to rule + cnr.rule.Exprs = append(cnr.rule.Exprs, e) + return nil +} diff --git a/runtime/docker/iptables.go b/runtime/docker/iptables.go deleted file mode 100644 index 6a724847d..000000000 --- a/runtime/docker/iptables.go +++ /dev/null @@ -1,360 +0,0 @@ -package docker - -import ( - "bytes" - "errors" - "fmt" - - "github.com/google/nftables" - "github.com/google/nftables/expr" - "github.com/google/nftables/xt" - log "github.com/sirupsen/logrus" - "github.com/srl-labs/containerlab/utils" -) - -const ( - DockerFWUserChain = "DOCKER-USER" - DockerFWTable = "filter" - - IPTablesRuleComment = "set by containerlab" - - IPTablesCommentMaxSize = 256 -) - -type nftablesClient struct { - nftConn *nftables.Conn -} - -func newNftablesClient() (*nftablesClient, error) { - // setup netlink connection with nftables - nftConn, err := nftables.New(nftables.AsLasting()) - if err != nil { - return nil, err - } - - nftC := &nftablesClient{ - nftConn: nftConn, - } - - return nftC, nil -} - -func (nftC *nftablesClient) GetChains(name string) ([]*nftables.Chain, error) { - var result []*nftables.Chain - - chains, err := nftC.nftConn.ListChains() - if err != nil { - return nil, err - } - - for _, c := range chains { - if c.Name == name { - result = append(result, c) - } - } - return result, nil -} - -func (nftC *nftablesClient) GetTable(family nftables.TableFamily, name string) (*nftables.Table, error) { - tables, err := nftC.nftConn.ListTablesOfFamily(family) - if err != nil { - return nil, err - } - for _, t := range tables { - if t.Name == name { - return t, nil - } - } - return nil, fmt.Errorf("table %q not found", name) -} - -func (nftC *nftablesClient) DeleteRule(r *nftables.Rule) { - nftC.nftConn.DelRule(r) -} - -// GetRules returns all rules for the provided chain name, table name and family. -func (nftC *nftablesClient) GetRules(chainName, tableName string, family nftables.TableFamily) ([]*nftables.Rule, error) { - // get chain reference - chains, err := nftC.GetChains(chainName) - if err != nil { - return nil, err - } - - for _, c := range chains { - if c.Table.Name == tableName && c.Table.Family == family { - return nftC.nftConn.GetRules(c.Table, c) - } - } - - return nil, fmt.Errorf("no match for chain %q, table %q with family %q found", chainName, tableName, family) -} - -type clabNftablesRule struct { - rule *nftables.Rule -} - -func (nftC *nftablesClient) newClabNftablesRule(chainName, tableName string, family nftables.TableFamily, position uint64) (*clabNftablesRule, error) { - chains, err := nftC.GetChains(chainName) - if err != nil { - return nil, err - } - - for _, c := range chains { - if c.Table.Name == tableName && c.Table.Family == family { - r := &nftables.Rule{ - Handle: 0, - Position: position, - Table: c.Table, - Chain: c, - Exprs: []expr.Any{}, - } - - return &clabNftablesRule{rule: r}, nil - } - } - - return nil, errors.New("chain " + chainName + " not found in table " + tableName + " with family " + string(family)) - -} - -func (cnr *clabNftablesRule) AddOutputInterfaceFilter(oif string) { - // define the metadata to evaluate - metaOifName := &expr.Meta{ - Key: expr.MetaKeyOIFNAME, - SourceRegister: false, - Register: 1, - } - // define the comparison - comp := &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte(oif + "\x00"), - } - - // add expr to rule - cnr.rule.Exprs = append(cnr.rule.Exprs, metaOifName, comp) -} - -func (cnr *clabNftablesRule) AddCounter() error { - c := &expr.Counter{} - cnr.rule.Exprs = append(cnr.rule.Exprs, c) - return nil -} - -func (cnr *clabNftablesRule) AddVerdictAccept() error { - v := &expr.Verdict{ - Kind: expr.VerdictAccept, - } - cnr.rule.Exprs = append(cnr.rule.Exprs, v) - return nil -} - -func (cnr *clabNftablesRule) AddVerdictDrop() error { - v := &expr.Verdict{ - Kind: expr.VerdictDrop, - } - cnr.rule.Exprs = append(cnr.rule.Exprs, v) - return nil -} - -// AddComment adds a comment to the rule. -func (cnr *clabNftablesRule) AddComment(comment string) error { - // convert comment to byte - actualCommentByte := []byte(comment) - // check comment length not exceded - if len(actualCommentByte) > IPTablesCommentMaxSize { - return fmt.Errorf("comment max length is %d you've provided %d bytes", IPTablesCommentMaxSize, len(actualCommentByte)) - } - - // copy into byte alice of XT_MAX_COMMENT_LEN length - commentBytes := make([]byte, IPTablesCommentMaxSize) - copy(commentBytes, actualCommentByte) - - // create extension Info parameter as Unknown extension - commentXTInfo := xt.Unknown(commentBytes) - - // create the extension - e := &expr.Match{ - Name: "comment", - Info: &commentXTInfo, - } - // add extension to rule - cnr.rule.Exprs = append(cnr.rule.Exprs, e) - return nil -} - -// InsertRule inserts a rule. -func (nftC *nftablesClient) InsertRule(r *nftables.Rule) { - nftC.nftConn.InsertRule(r) -} - -// Flush sends all buffered commands in a single batch to nftables. -func (nftC *nftablesClient) Flush() error { - return nftC.nftConn.Flush() -} - -// Close closes connection to nftables. -func (nftC *nftablesClient) Close() { - nftC.nftConn.CloseLasting() -} - -// installIPTablesFwdRule 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) installIPTablesFwdRule() (err error) { - if !*d.mgmt.ExternalAccess { - return - } - - if d.mgmt.Bridge == "" { - log.Debug("skipping setup of forwarding rules for non-bridged management network") - return - } - - // first check if a rule already exists to not create duplicates - nftC, err := newNftablesClient() - if err != nil { - return err - } - defer nftC.Close() - - rules, err := nftC.GetRules(DockerFWUserChain, DockerFWTable, nftables.TableFamilyIPv4) - if err != nil { - return fmt.Errorf("%w. See http://containerlab.dev/manual/network/#external-access", err) - } - - if allowRuleForMgmtBrExists(d.mgmt.Bridge, rules) { - log.Debugf("found iptables forwarding rule targeting the bridge %q. Skipping creation of the forwarding rule.", d.mgmt.Bridge) - return nil - } - - log.Debugf("Installing iptables rules for bridge %q", d.mgmt.Bridge) - - // create a new rule - rule, err := nftC.newClabNftablesRule(DockerFWUserChain, DockerFWTable, nftables.TableFamilyIPv4, 0) - if err != nil { - return err - } - // set Output interface match - rule.AddOutputInterfaceFilter(d.mgmt.Bridge) - - // add a comment - err = rule.AddComment(IPTablesRuleComment) - if err != nil { - return err - } - // add a counter - err = rule.AddCounter() - if err != nil { - return err - } - // make it an ACCEPT rule - err = rule.AddVerdictAccept() - if err != nil { - return err - } - // mark and note for installation - nftC.InsertRule(rule.rule) - // flush changes out to nftables - nftC.Flush() - - 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 match and has a comment that is setup -// by containerlab. -func allowRuleForMgmtBrExists(brName string, rules []*nftables.Rule) bool { - return len(getRulesForMgmtBr(brName, rules)) > 0 -} - -// getRulesForMgmtBr returns all rules that have the provided bridge name in the output interface match -// and have a comment that is setup by containerlab. -func getRulesForMgmtBr(brName string, rules []*nftables.Rule) []*nftables.Rule { - var result []*nftables.Rule - - for _, r := range rules { - oifNameFound := false - commentFound := false - - for _, e := range r.Exprs { - switch v := e.(type) { - // Cmp is a comparison expression - // in the case of the rule we are looking for, it should be a comparison of the output interface name - case *expr.Cmp: - if bytes.Equal(v.Data, []byte(brName+"\x00")) { - oifNameFound = true - } - // Match is a match expression - // in the case of the rule we are looking for, it should contain - // a comment extension with the comment set by containerlab - case *expr.Match: - if v.Name == "comment" { - if val, ok := v.Info.(*xt.Unknown); ok { - if bytes.HasPrefix(*val, []byte(IPTablesRuleComment)) { - commentFound = true - } - } - } - default: - continue - } - } - - if oifNameFound && commentFound { - result = append(result, r) - } - } - - return result -} - -// deleteIPTablesFwdRule deletes `allow` rule installed with InstallIPTablesFwdRule when the bridge interface doesn't exist anymore. -func (d *DockerRuntime) deleteIPTablesFwdRule() (err error) { - if !*d.mgmt.ExternalAccess { - return - } - - br := d.mgmt.Bridge - - if br == "docker0" { - log.Debug("skipping deletion of iptables forwarding rule for non-bridged or default management network") - return - } - - // first check if a rule already exists to not create duplicates - nftC, err := newNftablesClient() - if err != nil { - return err - } - defer nftC.Close() - - rules, err := nftC.GetRules(DockerFWUserChain, DockerFWTable, nftables.TableFamilyIPv4) - if err != nil { - return fmt.Errorf("%w. See http://containerlab.dev/manual/network/#external-access", err) - } - - mgmtBrRules := getRulesForMgmtBr(d.mgmt.Bridge, rules) - if len(mgmtBrRules) == 0 { - log.Debug("external access iptables rule doesn't exist. Skipping deletion") - } - - // 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(br) - if err == nil { - log.Debugf("bridge %s is still in use, not removing the forwarding rule", br) - return nil - } - - log.Debugf("removing clab iptables rules for bridge %q", br) - for _, r := range mgmtBrRules { - nftC.DeleteRule(r) - } - - nftC.Flush() - - return nil -}