Skip to content
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
37 changes: 35 additions & 2 deletions nsjail_manager/nsjail/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,43 @@ import (
"fmt"
"log"
"os/exec"
"strings"
"syscall"
)

type command struct {
description string
cmd *exec.Cmd
ambientCaps []uintptr

// If ignoreErr isn't empty and this specific error occurs, suppress it (don’t log it, don’t return it).
ignoreErr string
}

func newCommand(
description string,
cmd *exec.Cmd,
ambientCaps []uintptr,
) *command {
return newCommandWithIgnoreErr(description, cmd, ambientCaps, "")
}

func newCommandWithIgnoreErr(
description string,
cmd *exec.Cmd,
ambientCaps []uintptr,
ignoreErr string,
) *command {
return &command{
description: description,
cmd: cmd,
ambientCaps: ambientCaps,
ignoreErr: ignoreErr,
}
}

func (cmd *command) isIgnorableError(err string) bool {
return cmd.ignoreErr != "" && strings.Contains(err, cmd.ignoreErr)
}

type commandRunner struct {
Expand All @@ -30,7 +60,7 @@ func (r *commandRunner) run() error {
}

output, err := command.cmd.CombinedOutput()
if err != nil {
if err != nil && !command.isIgnorableError(err.Error()) && !command.isIgnorableError(string(output)) {
return fmt.Errorf("failed to %s: %v, output: %s", command.description, err, output)
}
}
Expand All @@ -45,7 +75,10 @@ func (r *commandRunner) runIgnoreErrors() error {
}

output, err := command.cmd.CombinedOutput()
if err != nil {
if err != nil && !command.isIgnorableError(err.Error()) && !command.isIgnorableError(string(output)) {
log.Printf("err: %v", err)
log.Printf("")

log.Printf("failed to %s: %v, output: %s", command.description, err, output)
continue
}
Expand Down
4 changes: 2 additions & 2 deletions nsjail_manager/nsjail/jail.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ func (l *LinuxJail) ConfigureHostNsCommunication(pidInt int) error {
// This isolates the interface so that it becomes visible only inside the
// jail's netns. From this point on, the jail will configure its end of
// the veth pair (IP address, routes, etc.) independently of the host.
{
newCommand(
"Move jail-side veth into network namespace",
exec.Command("ip", "link", "set", l.vethJailName, "netns", PID),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
})
if err := runner.run(); err != nil {
return err
Expand Down
12 changes: 6 additions & 6 deletions nsjail_manager/nsjail/local_stub_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,29 @@ func ConfigureDNSForLocalStubResolver() error {
runner := newCommandRunner([]*command{
// Redirect all DNS queries inside the namespace to the host DNS listener.
// Needed because systemd-resolved listens on a host-side IP, not inside the namespace.
{
newCommand(
"Redirect DNS queries (DNAT 53 → host DNS)",
exec.Command("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", "DNAT", "--to-destination", "192.168.100.1:53"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Rewrite the SOURCE IP of redirected DNS packets.
// Required because DNS queries originating as 127.0.0.1 inside the namespace
// must not leave the namespace with a loopback source (kernel drops them).
// SNAT ensures packets arrive at systemd-resolved with a valid, routable source.
{
newCommand(
"Fix DNS source IP (SNAT 127.0.0.x → 192.168.100.2)",
exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", "-p", "udp", "--dport", "53", "-d", "192.168.100.1", "-j", "SNAT", "--to-source", "192.168.100.2"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Allow packets destined for 127.0.0.0/8 to go through routing and NAT.
// Without this, DNS queries to 127.0.0.53 never hit iptables OUTPUT
// and cannot be redirected to the host.
{
newCommand(
"Allow loopback-destined traffic to pass through NAT (route_localnet)",
// TODO(yevhenii): consider replacing with specific interfaces instead of all
exec.Command("sysctl", "-w", "net.ipv4.conf.all.route_localnet=1"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
})
if err := runner.run(); err != nil {
return err
Expand Down
53 changes: 27 additions & 26 deletions nsjail_manager/nsjail/networking_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,26 @@ func (l *LinuxJail) configureHostNetworkBeforeCmdExec() error {
// between the host and the jail namespace. One end stays on the host,
// the other will be moved into the jail. This provides a dedicated,
// isolated L2 network for the jail.
{
newCommand(
"Create host–jail veth interface pair",
exec.Command("ip", "link", "add", vethHostName, "type", "veth", "peer", "name", vethJailName),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Assign an IP address to the host side of the veth pair. The /24 mask
// implicitly defines the jail's entire subnet as 192.168.100.0/24.
// The host address (192.168.100.1) becomes the default gateway for
// processes inside the jail and is used by NAT and interception rules
// to route traffic out of the namespace.
{
newCommand(
"Assign IP to host-side veth",
exec.Command("ip", "addr", "add", "192.168.100.1/24", "dev", vethHostName),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
{
),
newCommand(
"Activate host-side veth interface",
exec.Command("ip", "link", "set", vethHostName, "up"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
})
if err := runner.run(); err != nil {
return err
Expand All @@ -62,11 +62,11 @@ func (l *LinuxJail) configureIptables() error {
// the jail's veth interface and the outside network. Without this,
// NAT and forwarding rules would have no effect because the kernel
// would drop transit packets.
{
newCommand(
"enable IP forwarding",
exec.Command("sysctl", "-w", "net.ipv4.ip_forward=1"),
[]uintptr{},
},
),
// Apply source NAT (MASQUERADE) for all traffic leaving the jail’s
// private subnet. This rewrites the source IP of packets originating
// from 192.168.100.0/24 to the host’s external interface IP. It enables:
Expand All @@ -77,11 +77,11 @@ func (l *LinuxJail) configureIptables() error {
//
// MASQUERADE is used instead of SNAT so it works even when the host IP
// changes dynamically.
{
newCommand(
"NAT rules for outgoing traffic (MASQUERADE for return traffic)",
exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", "-s", "192.168.100.0/24", "-j", "MASQUERADE"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Redirect *ALL TCP traffic* coming from the jail’s veth interface
// to the local HTTP/TLS-intercepting proxy. This causes *every* TCP
// connection (HTTP, HTTPS, plain TCP protocols) initiated by jailed
Expand All @@ -93,11 +93,11 @@ func (l *LinuxJail) configureIptables() error {
// REDIRECT rewrites the destination IP to 127.0.0.1 and the destination
// port to the HTTP proxy's port, forcing traffic through the proxy without
// requiring any configuration inside the jail.
{
newCommand(
"Route ALL TCP traffic to HTTP proxy",
exec.Command("iptables", "-t", "nat", "-A", "PREROUTING", "-i", l.vethHostName, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpProxyPort)),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Allow forwarding of non-TCP packets originating from the jail’s subnet.
// This rule is primarily needed for traffic that is *not* intercepted by
// the TCP REDIRECT rule — for example:
Expand All @@ -108,23 +108,23 @@ func (l *LinuxJail) configureIptables() error {
//
// Redirected TCP flows never reach the FORWARD chain (they are locally
// redirected in PREROUTING), so this rule does not apply to TCP traffic.
{
newCommand(
"Allow outbound non-TCP traffic from jail subnet",
exec.Command("iptables", "-A", "FORWARD", "-s", "192.168.100.0/24", "-j", "ACCEPT"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Allow forwarding of return traffic destined for the jail’s subnet for
// non-TCP flows. This complements the previous FORWARD rule and ensures
// that responses to DNS (UDP) or ICMP packets can reach the jail.
//
// As with the previous rule, this has no effect on TCP traffic because
// all TCP connections from the jail are intercepted and redirected to
// the local proxy before reaching the forwarding path.
{
newCommand(
"Allow inbound return traffic to jail subnet (non-TCP)",
exec.Command("iptables", "-A", "FORWARD", "-d", "192.168.100.0/24", "-j", "ACCEPT"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
})
if err := runner.run(); err != nil {
return err
Expand All @@ -137,11 +137,12 @@ func (l *LinuxJail) configureIptables() error {
// cleanupNetworking removes networking configuration
func (l *LinuxJail) cleanupNetworking() error {
runner := newCommandRunner([]*command{
{
newCommandWithIgnoreErr(
"delete veth pair",
exec.Command("ip", "link", "del", l.vethHostName),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
"Cannot find device",
),
})
if err := runner.runIgnoreErrors(); err != nil {
return err
Expand All @@ -153,26 +154,26 @@ func (l *LinuxJail) cleanupNetworking() error {
// cleanupIptables removes iptables rules
func (l *LinuxJail) cleanupIptables() error {
runner := newCommandRunner([]*command{
{
newCommand(
"Remove: NAT rules for outgoing traffic (MASQUERADE for return traffic)",
exec.Command("iptables", "-t", "nat", "-D", "POSTROUTING", "-s", "192.168.100.0/24", "-j", "MASQUERADE"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
{
),
newCommand(
"Remove: Route ALL TCP traffic to HTTP proxy",
exec.Command("iptables", "-t", "nat", "-D", "PREROUTING", "-i", l.vethHostName, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpProxyPort)),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
{
),
newCommand(
"Remove: Allow outbound non-TCP traffic from jail subnet",
exec.Command("iptables", "-D", "FORWARD", "-s", "192.168.100.0/24", "-j", "ACCEPT"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
{
),
newCommand(
"Remove: Allow inbound return traffic to jail subnet (non-TCP)",
exec.Command("iptables", "-D", "FORWARD", "-d", "192.168.100.0/24", "-j", "ACCEPT"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
})
if err := runner.runIgnoreErrors(); err != nil {
return err
Expand Down
17 changes: 8 additions & 9 deletions nsjail_manager/nsjail/networking_ns.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,36 @@ func SetupChildNetworking(vethNetJail string) error {
// matches the subnet defined on the host side (192.168.100.0/24),
// ensuring both interfaces appear on the same L2 network. This address
// (192.168.100.2) will serve as the jail's primary outbound source IP.
{
newCommand(
"Assign IP to jail-side veth",
exec.Command("ip", "addr", "add", "192.168.100.2/24", "dev", vethNetJail),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Bring the jail-side veth interface up. Until the interface is set UP,
// the jail cannot send or receive any packets on this link, even if the
// IP address and routes are configured correctly.
{
newCommand(
"Activate jail-side veth interface",
exec.Command("ip", "link", "set", vethNetJail, "up"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Bring the jail-side veth interface up. Until the interface is set UP,
// the jail cannot send or receive any packets on this link, even if the
// IP address and routes are configured correctly.
{
newCommand(
"Enable loopback interface in jail",
exec.Command("ip", "link", "set", "lo", "up"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
// Set the default route for all outbound traffic inside the jail. The
// gateway is the host-side veth address (192.168.100.1), which performs
// NAT and transparent TCP interception. This ensures that packets not
// destined for the jail subnet are routed to the host for processing.

{
newCommand(
"Configure default gateway for jail",
exec.Command("ip", "route", "add", "default", "via", "192.168.100.1"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
},
),
})
if err := runner.run(); err != nil {
return err
Expand Down
Loading