Skip to content
Closed
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
49 changes: 44 additions & 5 deletions packages/orchestrator/pkg/factories/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/cgroup"
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/nbd"
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/network"
networkv2 "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/network/v2"
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/template"
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/template/peerclient"
"github.com/e2b-dev/infra/packages/orchestrator/pkg/server"
Expand Down Expand Up @@ -718,12 +719,50 @@ func run(config cfg.Config, opts Options) (success bool) {
if err != nil {
logger.L().Fatal(ctx, "failed to create network pool", zap.Error(err))
}
networkPool := network.NewPool(network.NewSlotsPoolSize, network.ReusedSlotsPoolSize, slotStorage, config.NetworkConfig)
startService("network pool", func() error {
networkPool.Populate(ctx)
var networkPool network.PoolInterface
if config.NetworkConfig.NetworkVersion == 2 {
logger.L().Info(ctx, "using v2 network pool (nftables)")

return nil
})
if err := networkv2.ValidateV2Prerequisites(); err != nil {
logger.L().Fatal(ctx, "v2 network prerequisites not met", zap.Error(err))
}

hostFw, hfErr := networkv2.NewHostFirewall(network.DefaultGateway(), config.NetworkConfig)
if hfErr != nil {
logger.L().Fatal(ctx, "failed to create v2 host firewall", zap.Error(hfErr))
}

// Startup reclaim tore down every slot that survived the last run, so
// the surviving set is empty and this drops the elements they left
// behind. Skipped when reclaim is disabled: the live slots are then
// unknown, and flushing would cut them off.
if !config.DisableStartupReclaim {
if err := hostFw.ReconcileSlots(ctx, nil); err != nil {
logger.L().Fatal(ctx, "failed to reconcile v2 host firewall slots", zap.Error(err))
}
}

observer, obsErr := networkv2.NewVethObserver()
if obsErr != nil {
logger.L().Fatal(ctx, "failed to create v2 veth observer", zap.Error(obsErr))
}

v2Pool := networkv2.NewV2Pool(slotStorage, config.NetworkConfig, hostFw, observer)
startService("network pool", func() error {
v2Pool.Populate(ctx)

return nil
})
networkPool = v2Pool
} else {
v1Pool := network.NewPool(network.NewSlotsPoolSize, network.ReusedSlotsPoolSize, slotStorage, config.NetworkConfig)
startService("network pool", func() error {
v1Pool.Populate(ctx)

return nil
})
networkPool = v1Pool
}
closers = append(closers, closer{"network pool", networkPool.Close})

// sandbox factory
Expand Down
3 changes: 3 additions & 0 deletions packages/orchestrator/pkg/sandbox/network/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const loopbackInterface = "lo"
// Host default gateway name
var defaultGateway = utils.Must(getDefaultGateway(context.Background()))

// DefaultGateway returns the detected host default gateway interface name.
func DefaultGateway() string { return defaultGateway }

// func getDefaultGateway() (string, error) {
// route, err := exec.Command(
// "sh",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//go:build linux

package network

import (
"testing"

"github.com/stretchr/testify/require"
)

// An absent NETWORK_VERSION must select v1: the v2 datapath is opt-in.
func TestParseConfig_NetworkVersion(t *testing.T) { //nolint:paralleltest // t.Setenv
t.Run("absent selects v1", func(t *testing.T) { //nolint:paralleltest // t.Setenv
config, err := ParseConfig()
require.NoError(t, err)
require.Equal(t, 1, config.NetworkVersion)
})

t.Run("set but empty selects v1", func(t *testing.T) { //nolint:paralleltest // t.Setenv
t.Setenv("NETWORK_VERSION", "")

config, err := ParseConfig()
require.NoError(t, err)
require.Equal(t, 1, config.NetworkVersion)
})

t.Run("explicit 2 selects v2", func(t *testing.T) { //nolint:paralleltest // t.Setenv
t.Setenv("NETWORK_VERSION", "2")

config, err := ParseConfig()
require.NoError(t, err)
require.Equal(t, 2, config.NetworkVersion)
})

t.Run("non-numeric fails loudly", func(t *testing.T) { //nolint:paralleltest // t.Setenv
t.Setenv("NETWORK_VERSION", "two")

_, err := ParseConfig()
require.Error(t, err)
})
}
3 changes: 3 additions & 0 deletions packages/orchestrator/pkg/sandbox/network/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ type Config struct {
// an explicit 0 disables marking for builds only. Range 0..63.
// Set-but-empty behaves as unset (inherits): the env parser skips it.
BuildSandboxEgressDSCP *uint8 `env:"BUILD_SANDBOX_EGRESS_DSCP"`

// NetworkVersion selects v1 (iptables per-sandbox) or v2 (nftables, host sets).
NetworkVersion int `env:"NETWORK_VERSION" envDefault:"1"`
}

// EgressClass selects which configured egress DSCP applies to a sandbox. It
Expand Down
18 changes: 18 additions & 0 deletions packages/orchestrator/pkg/sandbox/network/pool_iface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build linux

package network

import (
"context"
"time"

"github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator"
)

// PoolInterface abstracts the network slot pool so v1 (*Pool) and v2 pools can
// be swapped. The existing *Pool already satisfies this interface.
type PoolInterface interface {
Get(ctx context.Context, network *orchestrator.SandboxNetworkConfig, class EgressClass) (*Slot, error)
ReturnAsync(ctx context.Context, slot *Slot, releasedFn ReleaseNotify, returnDelay time.Duration) error
Close(ctx context.Context) error
}
6 changes: 6 additions & 0 deletions packages/orchestrator/pkg/sandbox/network/slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"net"
"path/filepath"
"slices"
"strconv"
"sync/atomic"

Expand Down Expand Up @@ -496,6 +497,11 @@ func getHostNetworkCIDR() *net.IPNet {
return subnet
}

// TapHostHardwareAddr returns the fixed host-side tap MAC.
func TapHostHardwareAddr() net.HardwareAddr {
return slices.Clone(tapHostHardwareAddr)
}

// getTapHostHardwareAddr parses the fixed tapHostMAC constant once at package
// init, so CreateNetwork doesn't reparse a value that can never change or
// fail at runtime.
Expand Down
5 changes: 5 additions & 0 deletions packages/orchestrator/pkg/sandbox/network/storage_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ func (s *StorageLocal) Release(ips *Slot) error {
return nil
}

// IsNamespaceAvailable reports whether the named network namespace is free.
func IsNamespaceAvailable(dir, name string) (bool, error) {
return isNamespaceAvailable(dir, name)
}

func isNamespaceAvailable(dir, name string) (bool, error) {
nsPath := filepath.Join(dir, name)
_, err := os.Stat(nsPath)
Expand Down
36 changes: 36 additions & 0 deletions packages/orchestrator/pkg/sandbox/network/v2/absent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build linux

package v2

import (
"errors"
"fmt"
"os"

"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)

// appendUnlessAbsent records err unless it only reports state that is already
// gone, which makes teardown idempotent.
func appendUnlessAbsent(errs *[]error, err error, isAbsent func(error) bool, format string) {
if err == nil || isAbsent(err) {
return
}

*errs = append(*errs, fmt.Errorf(format, err))
}

func isRouteNotExist(err error) bool {
return errors.Is(err, unix.ESRCH) || errors.Is(err, unix.ENOENT)
}

func isLinkNotExist(err error) bool {
var linkNotFound netlink.LinkNotFoundError

return errors.As(err, &linkNotFound) || errors.Is(err, unix.ENODEV) || errors.Is(err, unix.ENOENT)
}

func isNamespaceNotExist(err error) bool {
return os.IsNotExist(err) || errors.Is(err, unix.ENOENT)
}
Loading
Loading