Skip to content

Commit de2f391

Browse files
authored
fix(orchestrator): harden shutdown network cleanup (#3000)
Return network slots synchronously during sandbox cleanup instead of fire-and-forget goroutines, guard firewall teardown against missing iptables state, and preserve ErrClosed when returning slots to a closed pool.
1 parent 057f20c commit de2f391

6 files changed

Lines changed: 330 additions & 123 deletions

File tree

‎packages/orchestrator/pkg/sandbox/network/firewall.go‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package network
44

55
import (
6+
"errors"
67
"fmt"
78
"net/netip"
89
"slices"
@@ -114,7 +115,16 @@ func (fw *Firewall) Close() error {
114115
fw.mu.Lock()
115116
defer fw.mu.Unlock()
116117

117-
return fw.conn.CloseLasting()
118+
fw.conn.DelTable(&nftables.Table{
119+
Name: tableName,
120+
Family: nftables.TableFamilyINet,
121+
})
122+
deleteErr := fw.conn.Flush()
123+
if errors.Is(deleteErr, unix.ENOENT) {
124+
deleteErr = nil
125+
}
126+
127+
return errors.Join(deleteErr, fw.conn.CloseLasting())
118128
}
119129

120130
// tapIfaceMatch returns expressions that match packets from the tap interface.

‎packages/orchestrator/pkg/sandbox/network/network.go‎

Lines changed: 95 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,75 @@ import (
77
"errors"
88
"fmt"
99
"net"
10+
"os"
1011
"runtime"
1112
"strconv"
1213

1314
"github.com/coreos/go-iptables/iptables"
1415
"github.com/vishvananda/netlink"
1516
"github.com/vishvananda/netns"
1617
"go.uber.org/zap"
18+
"golang.org/x/sys/unix"
1719

1820
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
1921
)
2022

21-
func (s *Slot) CreateNetwork(ctx context.Context) error {
23+
type notExistError interface {
24+
IsNotExist() bool
25+
}
26+
27+
type multiUnwrapError interface {
28+
Unwrap() []error
29+
}
30+
31+
func ignoreExpectedAbsent(err error, isExpected func(error) bool) bool {
32+
if err == nil {
33+
return true
34+
}
35+
36+
var joined multiUnwrapError
37+
if errors.As(err, &joined) {
38+
for _, child := range joined.Unwrap() {
39+
if !ignoreExpectedAbsent(child, isExpected) {
40+
return false
41+
}
42+
}
43+
44+
return true
45+
}
46+
47+
return isExpected(err)
48+
}
49+
50+
func isIPTablesNotExist(err error) bool {
51+
var notExist notExistError
52+
53+
return errors.As(err, &notExist) && notExist.IsNotExist()
54+
}
55+
56+
func isRouteNotExist(err error) bool {
57+
return errors.Is(err, unix.ESRCH) || errors.Is(err, unix.ENOENT)
58+
}
59+
60+
func isLinkNotExist(err error) bool {
61+
var linkNotFound netlink.LinkNotFoundError
62+
63+
return errors.As(err, &linkNotFound) || errors.Is(err, unix.ENODEV) || errors.Is(err, unix.ENOENT)
64+
}
65+
66+
func isNamespaceNotExist(err error) bool {
67+
return os.IsNotExist(err) || errors.Is(err, unix.ENOENT)
68+
}
69+
70+
func appendUnlessExpectedAbsentf(errs *[]error, err error, isExpected func(error) bool, format string) {
71+
if ignoreExpectedAbsent(err, isExpected) {
72+
return
73+
}
74+
75+
*errs = append(*errs, fmt.Errorf(format, err))
76+
}
77+
78+
func (s *Slot) CreateNetwork(ctx context.Context) (retErr error) {
2279
// Prevent thread changes so we can safely manipulate with namespaces
2380
runtime.LockOSThread()
2481
defer runtime.UnlockOSThread()
@@ -29,10 +86,19 @@ func (s *Slot) CreateNetwork(ctx context.Context) error {
2986
return fmt.Errorf("cannot get current (host) namespace: %w", err)
3087
}
3188

89+
cleanupNeeded := false
3290
defer func() {
33-
err = netns.Set(hostNS)
34-
if err != nil {
35-
logger.L().Error(ctx, "error resetting network namespace back to the host namespace", zap.Error(err))
91+
restoreErr := netns.Set(hostNS)
92+
if restoreErr != nil {
93+
logger.L().Error(ctx, "error resetting network namespace back to the host namespace", zap.Error(restoreErr))
94+
}
95+
96+
if retErr != nil && cleanupNeeded {
97+
if restoreErr != nil {
98+
retErr = errors.Join(retErr, fmt.Errorf("error resetting network namespace back to the host namespace before cleanup: %w", restoreErr))
99+
} else if cleanupErr := s.RemoveNetwork(); cleanupErr != nil {
100+
retErr = errors.Join(retErr, fmt.Errorf("error cleaning up partially created network: %w", cleanupErr))
101+
}
36102
}
37103

38104
err = hostNS.Close()
@@ -46,6 +112,7 @@ func (s *Slot) CreateNetwork(ctx context.Context) error {
46112
if err != nil {
47113
return fmt.Errorf("cannot create new namespace: %w", err)
48114
}
115+
cleanupNeeded = true
49116

50117
defer ns.Close()
51118

@@ -273,85 +340,67 @@ func (s *Slot) RemoveNetwork() error {
273340
} else {
274341
// Delete host forwarding rules
275342
err = tables.Delete("filter", "FORWARD", "-i", s.VethName(), "-o", defaultGateway, "-j", "ACCEPT")
276-
if err != nil {
277-
errs = append(errs, fmt.Errorf("error deleting host forwarding rule to default gateway: %w", err))
278-
}
343+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "error deleting host forwarding rule to default gateway: %w")
279344

280345
err = tables.Delete("filter", "FORWARD", "-i", defaultGateway, "-o", s.VethName(), "-j", "ACCEPT")
281-
if err != nil {
282-
errs = append(errs, fmt.Errorf("error deleting host forwarding rule from default gateway: %w", err))
283-
}
346+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "error deleting host forwarding rule from default gateway: %w")
284347

285348
// Delete host postrouting rules
286349
err = tables.Delete("nat", "POSTROUTING", "-s", s.HostCIDR(), "-o", defaultGateway, "-j", "MASQUERADE")
287-
if err != nil {
288-
errs = append(errs, fmt.Errorf("error deleting host postrouting rule: %w", err))
289-
}
350+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "error deleting host postrouting rule: %w")
290351

291352
// Delete hyperloop proxy redirect rule
292353
err = tables.Delete(
293354
"nat", "PREROUTING", "-i", s.VethName(),
294355
"-p", "tcp", "-d", s.config.OrchestratorInSandboxIPAddress, "--dport", "80",
295356
"-j", "REDIRECT", "--to-port", s.hyperloopPort,
296357
)
297-
if err != nil {
298-
errs = append(errs, fmt.Errorf("error deleting sandbox hyperloop proxy redirect rule: %w", err))
299-
}
358+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "error deleting sandbox hyperloop proxy redirect rule: %w")
300359

301360
// Delete changes made by egress proxy
302361
err = s.egressProxy.OnSlotDelete(s, tables)
303-
if err != nil {
304-
errs = append(errs, err)
305-
}
362+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "%w")
306363
}
307364

308365
// Delete routing from host to FC namespace
309366
err = netlink.RouteDel(&netlink.Route{
310367
Gw: s.VpeerIP(),
311368
Dst: s.HostNet(),
312369
})
313-
if err != nil {
314-
errs = append(errs, fmt.Errorf("error deleting route from host to FC: %w", err))
315-
}
370+
appendUnlessExpectedAbsentf(&errs, err, isRouteNotExist, "error deleting route from host to FC: %w")
316371

317372
// Delete veth device
318373
// We explicitly delete the veth device from the host namespace because even though deleting
319374
// is deleting the device there may be a race condition when creating a new veth device with
320375
// the same name immediately after deleting the namespace.
321376
veth, err := netlink.LinkByName(s.VethName())
322377
if err != nil {
323-
errs = append(errs, fmt.Errorf("error finding veth: %w", err))
378+
appendUnlessExpectedAbsentf(&errs, err, isLinkNotExist, "error finding veth: %w")
324379
} else {
325380
err = netlink.LinkDel(veth)
326-
if err != nil {
327-
errs = append(errs, fmt.Errorf("error deleting veth device: %w", err))
328-
}
381+
appendUnlessExpectedAbsentf(&errs, err, isLinkNotExist, "error deleting veth device: %w")
329382
}
330383

331-
// Delete NFS proxy redirect rule
332-
err = tables.Delete("nat", "PREROUTING",
333-
"--in-interface", s.VethName(), "--protocol", "tcp",
334-
"--destination", s.config.OrchestratorInSandboxIPAddress, "--dport", "2049",
335-
"--jump", "REDIRECT", "--to-port", strconv.Itoa(int(s.config.NFSProxyPort)),
336-
)
337-
if err != nil {
338-
errs = append(errs, fmt.Errorf("error deleting sandbox NFS proxy redirect rule: %w", err))
339-
}
384+
if tables != nil {
385+
// Delete NFS proxy redirect rule
386+
err = tables.Delete("nat", "PREROUTING",
387+
"--in-interface", s.VethName(), "--protocol", "tcp",
388+
"--destination", s.config.OrchestratorInSandboxIPAddress, "--dport", "2049",
389+
"--jump", "REDIRECT", "--to-port", strconv.Itoa(int(s.config.NFSProxyPort)),
390+
)
391+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "error deleting sandbox NFS proxy redirect rule: %w")
340392

341-
// Delete portmapper redirect rule
342-
err = tables.Delete("nat", "PREROUTING",
343-
"--in-interface", s.VethName(), "--protocol", "tcp",
344-
"--destination", s.config.OrchestratorInSandboxIPAddress, "--dport", "111",
345-
"--jump", "REDIRECT", "--to-port", strconv.Itoa(int(s.config.PortmapperPort)),
346-
)
347-
if err != nil {
348-
errs = append(errs, fmt.Errorf("error deleting sandbox portmapper redirect rule: %w", err))
393+
// Delete portmapper redirect rule
394+
err = tables.Delete("nat", "PREROUTING",
395+
"--in-interface", s.VethName(), "--protocol", "tcp",
396+
"--destination", s.config.OrchestratorInSandboxIPAddress, "--dport", "111",
397+
"--jump", "REDIRECT", "--to-port", strconv.Itoa(int(s.config.PortmapperPort)),
398+
)
399+
appendUnlessExpectedAbsentf(&errs, err, isIPTablesNotExist, "error deleting sandbox portmapper redirect rule: %w")
349400
}
350401

351402
err = netns.DeleteNamed(s.NamespaceID())
352-
if err != nil {
353-
errs = append(errs, fmt.Errorf("error deleting namespace: %w", err))
354-
}
403+
appendUnlessExpectedAbsentf(&errs, err, isNamespaceNotExist, "error deleting namespace: %w")
355404

356405
return errors.Join(errs...)
357406
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//go:build linux
2+
3+
package network
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
"golang.org/x/sys/unix"
13+
)
14+
15+
type fakeNotExistError struct {
16+
notExist bool
17+
}
18+
19+
func (e fakeNotExistError) Error() string {
20+
return "fake iptables error"
21+
}
22+
23+
func (e fakeNotExistError) IsNotExist() bool {
24+
return e.notExist
25+
}
26+
27+
func TestIgnoreExpectedAbsentHandlesWrappedAndJoinedErrors(t *testing.T) {
28+
t.Parallel()
29+
30+
wrapped := fmt.Errorf("wrapped: %w", fakeNotExistError{notExist: true})
31+
joined := errors.Join(wrapped, fakeNotExistError{notExist: true})
32+
33+
require.True(t, ignoreExpectedAbsent(joined, isIPTablesNotExist))
34+
require.False(t, ignoreExpectedAbsent(errors.Join(joined, errors.New("boom")), isIPTablesNotExist))
35+
require.False(t, ignoreExpectedAbsent(fakeNotExistError{notExist: false}, isIPTablesNotExist))
36+
}
37+
38+
func TestExpectedAbsentClassifiers(t *testing.T) {
39+
t.Parallel()
40+
41+
require.True(t, isRouteNotExist(fmt.Errorf("route delete failed: %w", unix.ESRCH)))
42+
require.False(t, isRouteNotExist(unix.EPERM))
43+
44+
require.True(t, isLinkNotExist(fmt.Errorf("link delete failed: %w", unix.ENODEV)))
45+
require.False(t, isLinkNotExist(unix.EPERM))
46+
47+
require.True(t, isNamespaceNotExist(&os.PathError{Op: "remove", Path: "/var/run/netns/missing", Err: unix.ENOENT}))
48+
require.False(t, isNamespaceNotExist(unix.EPERM))
49+
}

0 commit comments

Comments
 (0)