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
34 changes: 32 additions & 2 deletions internal/ateomnet/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ func PodIPv4() (net.IP, error) {
}

// EnableIPv4Forwarding enables IPv4 forwarding in the current network namespace.
// It also enables IPv6 forwarding so actor IPv6 traffic (including DNS queries
// on IPv6-capable clusters) is routed between the veth and eth0 instead of
// being dropped by ip6_forward().
func EnableIPv4Forwarding() error {
// Forwarding is required because actor packets now enter the worker pod via
// the host-side veth and then leave through the pod's eth0. Without this, the
Expand All @@ -203,20 +206,47 @@ func EnableIPv4Forwarding() error {
// The worker holds CAP_SYS_ADMIN and uses no user namespace, so the ro flag
// is not locked: clear it, write the sysctl, restore ro.
const path = "/proc/sys/net/ipv4/ip_forward"
if err := writeSysctlIfUnset(path); err != nil {
return fmt.Errorf("while enabling IPv4 forwarding in worker pod netns: %w", err)
}
// IPv6 forwarding: actor packets that arrive on the veth and leave via eth0
// are IPv6 on dual-stack / IPv6-only clusters. Without
// net.ipv6.conf.all.forwarding the kernel drops every IPv6 packet in
// ip6_forward(), including the actor's DNS queries. conf.all.forwarding=1
// also implies the per-interface default, so a single write covers the veth
// and eth0.
const v6path = "/proc/sys/net/ipv6/conf/all/forwarding"
if err := writeSysctlIfUnset(v6path); err != nil {
return fmt.Errorf("while enabling IPv6 forwarding in worker pod netns: %w", err)
}
return nil
}

// writeSysctlIfUnset writes "1\n" to a sysctl path unless it already reads "1".
// If the path does not exist (e.g. IPv6 sysctls on a kernel with IPv6 disabled),
// it returns nil — IPv6 forwarding is simply unavailable, not an error.
func writeSysctlIfUnset(path string) error {
if b, err := os.ReadFile(path); err == nil && len(b) > 0 && b[0] == '1' {
return nil
}
if err := os.WriteFile(path, []byte("1\n"), 0o644); err == nil {
return nil
}
if _, err := os.Stat(path); os.IsNotExist(err) {
// Path absent (e.g. IPv6 disabled in kernel): nothing to enable.
return nil
}
// Without privileged, the container runtime bind-mounts /proc/sys read-only.
// The worker holds CAP_SYS_ADMIN and uses no user namespace, so the ro flag
// is not locked: clear it, write the sysctl, restore ro.
if err := unix.Mount("none", "/proc/sys", "", unix.MS_BIND|unix.MS_REMOUNT, ""); err != nil {
return fmt.Errorf("while remounting /proc/sys read-write to enable IPv4 forwarding: %w", err)
return fmt.Errorf("while remounting /proc/sys read-write to enable forwarding: %w", err)
}
defer func() {
_ = unix.Mount("none", "/proc/sys", "", unix.MS_BIND|unix.MS_REMOUNT|unix.MS_RDONLY, "")
}()
if err := os.WriteFile(path, []byte("1\n"), 0o644); err != nil {
return fmt.Errorf("while enabling IPv4 forwarding in worker pod netns: %w", err)
return fmt.Errorf("while writing %s: %w", path, err)
}
return nil
}
Expand Down
84 changes: 84 additions & 0 deletions internal/ateomnet/write_sysctl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//go:build linux

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ateomnet

import (
"os"
"path/filepath"
"testing"
)

// TestWriteSysctlIfUnset verifies writeSysctlIfUnset's fast paths against a
// temp file standing in for a /proc/sys node: it must not rewrite a value
// that already reads "1", and it must write "1\n" when the value is missing
// or unset. The privileged bind-remount path is covered by the netns
// integration tests (withTestNetNS), which require root.
func TestWriteSysctlIfUnset(t *testing.T) {
dir := t.TempDir()

t.Run("already_set", func(t *testing.T) {
p := filepath.Join(dir, "already")
// Sentinel content: if writeSysctlIfUnset rewrote the file, the value
// would change to "1\n" and this assertion would fail. Keeping the
// file larger than the helper's output makes a silent rewrite
// detectable.
if err := os.WriteFile(p, []byte("1 other-content\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := writeSysctlIfUnset(p); err != nil {
t.Fatalf("writeSysctlIfUnset: %v", err)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if string(b) != "1 other-content\n" {
t.Fatalf("already-set file was rewritten: %q", b)
}
})

t.Run("unset_written", func(t *testing.T) {
p := filepath.Join(dir, "unset")
if err := writeSysctlIfUnset(p); err != nil {
t.Fatalf("writeSysctlIfUnset: %v", err)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if len(b) < 1 || b[0] != '1' {
t.Fatalf("expected '1' written, got %q", b)
}
})

t.Run("zero_is_rewritten", func(t *testing.T) {
p := filepath.Join(dir, "zero")
if err := os.WriteFile(p, []byte("0\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := writeSysctlIfUnset(p); err != nil {
t.Fatalf("writeSysctlIfUnset: %v", err)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if len(b) < 1 || b[0] != '1' {
t.Fatalf("expected '1' written, got %q", b)
}
})
}