Skip to content
Open
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
46 changes: 42 additions & 4 deletions runtime/docker/firewall/iptables/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package iptables
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"syscall"

"github.com/charmbracelet/log"
"github.com/google/shlex"
Expand Down Expand Up @@ -101,7 +103,7 @@ func (c *IpTablesClient) InstallForwardingRulesForAF(

log.Debugf("Installing iptables (%s) rules for bridge %q", af, iface)

stdOutErr, err := exec.Command(iptCmd, cmd...).CombinedOutput()
stdOutErr, err := newIptablesCmd(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)
Expand Down Expand Up @@ -137,7 +139,7 @@ func (c *IpTablesClient) DeleteForwardingRulesForAF(
iface := rule.Interface

// first check if a rule exists before trying to delete it
res, err := exec.Command(iptCmd, strings.Split(iptCheckArgs, " ")...).Output()
res, err := newIptablesCmd(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
Expand Down Expand Up @@ -170,7 +172,7 @@ func (c *IpTablesClient) DeleteForwardingRulesForAF(
log.Debugf("removing clab iptables rules for bridge %q", iface)
log.Debugf("trying to delete the forwarding rule with cmd: iptables %s", cmd)

stdOutErr, err := exec.Command(iptCmd, cmd...).CombinedOutput()
stdOutErr, err := newIptablesCmd(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)
Expand All @@ -183,7 +185,7 @@ func (c *IpTablesClient) DeleteForwardingRulesForAF(
func (c *IpTablesClient) ruleExists(af string, rule *definitions.FirewallRule) bool {
iptCmd := iptablesCmd[af]

res, err := exec.Command(iptCmd, strings.Split(iptCheckArgs, " ")...).CombinedOutput()
res, err := newIptablesCmd(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
Expand Down Expand Up @@ -215,3 +217,39 @@ func (c *IpTablesClient) ruleExists(af string, rule *definitions.FirewallRule) b

return false
}

// newIptablesCmd builds an iptables/ip6tables command that is safe to run from a
// setuid-root containerlab binary (ruid=user, euid=0).
//
// iptables ≥ 1.8.8 exits 111 when getuid() != geteuid() because it loads match/
// target shared libraries and therefore refuses to run under a setuid parent.
// We isolate the fix to the child process via Credential so the long-lived
// containerlab process keeps its real UID (important for $HOME file ownership).
// Env is cleared so a poisoned library path cannot follow into iptables.
func newIptablesCmd(name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...)
// nil Env inherits the parent; empty means env -i.
cmd.Env = []string{}

if attr := iptablesSysProcAttr(os.Getuid(), os.Geteuid(), os.Getegid()); attr != nil {
cmd.SysProcAttr = attr
}

return cmd
}

// iptablesSysProcAttr returns SysProcAttr that sets the child's real/effective
// UIDs to root when the parent is running setuid-root. Nil when no change is needed.
func iptablesSysProcAttr(ruid, euid, egid int) *syscall.SysProcAttr {
if euid != 0 || ruid == 0 {
return nil
}

return &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: 0,
Gid: uint32(egid),
NoSetGroups: true,
},
}
}
45 changes: 45 additions & 0 deletions runtime/docker/firewall/iptables/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package iptables

import (
"testing"
)

func TestIptablesSysProcAttr(t *testing.T) {
t.Run("setuid parent needs matching root uids in child", func(t *testing.T) {
attr := iptablesSysProcAttr(1000, 0, 1000)
if attr == nil || attr.Credential == nil {
t.Fatal("expected Credential for setuid-root parent")
}
if attr.Credential.Uid != 0 {
t.Fatalf("Uid = %d, want 0", attr.Credential.Uid)
}
if attr.Credential.Gid != 1000 {
t.Fatalf("Gid = %d, want 1000 (preserve egid)", attr.Credential.Gid)
}
if !attr.Credential.NoSetGroups {
t.Fatal("expected NoSetGroups to preserve supplementary groups")
}
})

t.Run("fully root parent needs no credential", func(t *testing.T) {
if attr := iptablesSysProcAttr(0, 0, 0); attr != nil {
t.Fatalf("expected nil SysProcAttr, got %#v", attr)
}
})

t.Run("non-root parent needs no credential", func(t *testing.T) {
if attr := iptablesSysProcAttr(1000, 1000, 1000); attr != nil {
t.Fatalf("expected nil SysProcAttr, got %#v", attr)
}
})
}

func TestNewIptablesCmdClearsEnv(t *testing.T) {
cmd := newIptablesCmd("iptables", "-V")
if cmd.Env == nil {
t.Fatal("Env is nil (would inherit parent); want empty slice")
}
if len(cmd.Env) != 0 {
t.Fatalf("Env = %#v, want empty", cmd.Env)
}
}