-
Notifications
You must be signed in to change notification settings - Fork 261
feat: dynamic egress network rule updates for running sandboxes #2074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
levb
wants to merge
45
commits into
main
Choose a base branch
from
lev-allow-deny-dynamic
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,663
−505
Open
Changes from 36 commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
2fbecf5
feat(api): add PUT /sandboxes/{sandboxID}/network endpoint (stage 1)
levb af2b869
fix(test): use valid-format sandbox ID in TestUpdateNetworkConfig_Not…
levb d541327
feat(orchestrator): wire dynamic network egress updates end-to-end
levb 2db7190
refactor(api): simplify UpdateSandboxNetworkConfig return and remove …
levb 4511929
feat(api): support domain filtering in network update path
levb d4f93b8
test: add pause/resume persistence test for dynamic network updates
levb 027a24a
fix: address revive lint for redundant if-return in update_network
levb f8c876f
test: add domain allow/remove tests for dynamic network updates
levb d5c8fbc
refactor: merge 3 RWMutexes into 1, guarantee GetNetwork() non-nil, u…
levb 286c6f1
fix: validate CIDRs before firewall reset, add 409 to OpenAPI spec, u…
levb ef4708b
chore: auto-commit generated changes
github-actions[bot] cc5e43d
fix(test): initialize networkEgress in TestIsEgressAllowed
levb c452eae
Merge branch 'lev-allow-deny-dynamic' of github.com:e2b-dev/infra int…
levb 8bd0c92
fix(api): persist store update only after gRPC node update succeeds
levb 942d1e1
refactor: migrate all callers to atomic ReplaceUserRules, remove one-…
levb 66dcfbb
chore: auto-commit generated changes
github-actions[bot] b986f2c
fix: bypass firewall_toolkit validation for 0.0.0.0/0 CIDR
levb 1a95c13
lint
levb 79cc831
fix(api): validate denyOut and domain rules in network update endpoint
levb 8bf22fa
test: consolidate network update integration tests into comprehensive…
levb da52196
PR feedback: extract shared egress validation and use ReportErrorByCode
levb 3e5923b
PR feedback: embed SandboxNetworkEgressConfig in update network request
levb 73d370a
PR feedback: move state check into updateFunc to match KeepAliveFor p…
levb 9417d82
PR feedback: always set firewallCustomRules after UpdateInternet
levb b853ee4
lint: simplify redundant if-return in updateSandboxNetworkOnNode call
levb dcbc89d
PR feedback: merge UpdateNetwork into unified Update RPC with transac…
levb ed87f50
PR feedback: remove unnecessary networkEgress init from ResumeSandbox
levb 0b2193b
Merge branch 'main' of github.com:e2b-dev/infra into lev-allow-deny-d…
levb 9524334
fix: restore networkEgress init in ResumeSandbox
levb 31175f4
Merge branch 'main' of github.com:e2b-dev/infra into lev-allow-deny-d…
levb 2c46352
PR feedback: unify egress config building and add domain validation
levb 50fdf2f
PR feedback: extract ApplyAllOrRollback utility for transactional upd…
levb f8c2100
PR feedback: move network config into Config with own RWMutex
levb e1cc432
PR feedback: handle wildcard domains in IDNA validation
levb fe7a6db
PR feedback: rename ApplyAllOrRollback to ApplyAllOrNone, fix test
levb a457ea6
Merge branch 'main' of github.com:e2b-dev/infra into lev-allow-deny-d…
levb a5d435c
PR feedback: removed questionable domain "validation"
levb aa11b20
PR feedback: restored accidentally removed comment
levb bc61dae
PR feedback: clarify nftables buffering semantics in clearAndReplaceC…
levb 15ee24a
PR feedback: add NewConfig constructor to guarantee non-nil network c…
levb 6f21b57
Merge branch 'main' of github.com:e2b-dev/infra into lev-allow-deny-d…
levb 19f2de9
Fix and simplify sandboxes_update_test.go after merge with main
levb d8be629
Fix nlreturn lint: add blank line before return in clearAndReplaceCIDRs
levb e1eee7f
Fix data race on network egress/ingress config access
levb 5d7abe5
Merge branch 'main' of github.com:e2b-dev/infra into lev-allow-deny-d…
levb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
|
|
||
| "github.com/e2b-dev/infra/packages/api/internal/api" | ||
| "github.com/e2b-dev/infra/packages/api/internal/utils" | ||
| "github.com/e2b-dev/infra/packages/auth/pkg/auth" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" | ||
| ) | ||
|
|
||
| func (a *APIStore) PutSandboxesSandboxIDNetwork( | ||
| c *gin.Context, | ||
| sandboxID string, | ||
| ) { | ||
| ctx := c.Request.Context() | ||
|
|
||
| var err error | ||
| sandboxID, err = utils.ShortID(sandboxID) | ||
| if err != nil { | ||
| a.sendAPIStoreError(c, http.StatusBadRequest, "Invalid sandbox ID") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| team := auth.MustGetTeamInfo(c) | ||
|
|
||
| body, err := utils.ParseBody[api.PutSandboxesSandboxIDNetworkJSONBody](ctx, c) | ||
| if err != nil { | ||
| a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Error when parsing request: %s", err)) | ||
| telemetry.ReportCriticalError(ctx, "error when parsing request", err) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| var allowedEntries []string | ||
| if body.AllowOut != nil { | ||
| allowedEntries = *body.AllowOut | ||
| } | ||
|
|
||
| var deniedEntries []string | ||
| if body.DenyOut != nil { | ||
| deniedEntries = *body.DenyOut | ||
| } | ||
|
|
||
| if apiErr := validateEgressRules(allowedEntries, deniedEntries); apiErr != nil { | ||
levb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| if apiErr := a.orchestrator.UpdateSandboxNetworkConfig(ctx, team.ID, sandboxID, allowedEntries, deniedEntries); apiErr != nil { | ||
| telemetry.ReportErrorByCode(ctx, apiErr.Code, "error updating sandbox network config", apiErr.Err) | ||
| a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| c.Status(http.StatusNoContent) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package orchestrator | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
|
|
||
| "github.com/google/uuid" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| "go.opentelemetry.io/otel/trace" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/status" | ||
|
|
||
| "github.com/e2b-dev/infra/packages/api/internal/api" | ||
| "github.com/e2b-dev/infra/packages/api/internal/sandbox" | ||
| "github.com/e2b-dev/infra/packages/api/internal/utils" | ||
| "github.com/e2b-dev/infra/packages/db/pkg/types" | ||
| orchestratorgrpc "github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" | ||
| ) | ||
|
|
||
| func (o *Orchestrator) UpdateSandboxNetworkConfig( | ||
levb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ctx context.Context, | ||
| teamID uuid.UUID, | ||
| sandboxID string, | ||
| allowedEntries []string, | ||
| deniedEntries []string, | ||
| ) *api.APIError { | ||
| egress := buildEgressConfig(allowedEntries, deniedEntries) | ||
|
|
||
| updateFunc := func(sbx sandbox.Sandbox) (sandbox.Sandbox, error) { | ||
| if sbx.State != sandbox.StateRunning { | ||
| return sbx, &sandbox.NotRunningError{SandboxID: sandboxID, State: sbx.State} | ||
| } | ||
|
|
||
| if sbx.Network == nil { | ||
| sbx.Network = &types.SandboxNetworkConfig{} | ||
| } | ||
|
|
||
| sbx.Network.Egress = &types.SandboxNetworkEgressConfig{ | ||
| AllowedAddresses: allowedEntries, | ||
| DeniedAddresses: deniedEntries, | ||
| } | ||
|
|
||
| return sbx, nil | ||
| } | ||
|
|
||
| var sbxNotFoundErr *sandbox.NotFoundError | ||
| var sbxNotRunningErr *sandbox.NotRunningError | ||
levb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| sbx, err := o.sandboxStore.Update(ctx, teamID, sandboxID, updateFunc) | ||
| if err != nil { | ||
| switch { | ||
| case errors.As(err, &sbxNotRunningErr): | ||
| return &api.APIError{Code: http.StatusConflict, ClientMsg: utils.SandboxChangingStateMsg(sandboxID, sbxNotRunningErr.State), Err: err} | ||
| case errors.As(err, &sbxNotFoundErr): | ||
| return &api.APIError{Code: http.StatusNotFound, ClientMsg: utils.SandboxNotFoundMsg(sandboxID), Err: err} | ||
| default: | ||
| return &api.APIError{Code: http.StatusInternalServerError, ClientMsg: "Error updating sandbox network config", Err: err} | ||
| } | ||
| } | ||
|
|
||
| // Apply the network update on the orchestrator node. | ||
| return o.updateSandboxNetworkOnNode(ctx, sbx, egress) | ||
| } | ||
|
|
||
| func (o *Orchestrator) updateSandboxNetworkOnNode( | ||
| ctx context.Context, | ||
| sbx sandbox.Sandbox, | ||
| egress *orchestratorgrpc.SandboxNetworkEgressConfig, | ||
| ) *api.APIError { | ||
| ctx, span := tracer.Start(ctx, "update-sandbox-network-on-node", | ||
| trace.WithAttributes( | ||
| attribute.String("instance.id", sbx.SandboxID), | ||
| ), | ||
| ) | ||
| defer span.End() | ||
|
|
||
| node := o.GetNode(sbx.ClusterID, sbx.NodeID) | ||
| if node == nil { | ||
| return &api.APIError{ | ||
| Code: http.StatusInternalServerError, | ||
| ClientMsg: fmt.Sprintf("Node hosting sandbox '%s' not found", sbx.SandboxID), | ||
| Err: fmt.Errorf("node '%s' not found for cluster '%s'", sbx.NodeID, sbx.ClusterID), | ||
| } | ||
| } | ||
|
|
||
| client, ctx := node.GetClient(ctx) | ||
| _, err := client.Sandbox.Update(ctx, &orchestratorgrpc.SandboxUpdateRequest{ | ||
| SandboxId: sbx.SandboxID, | ||
| Egress: egress, | ||
| }) | ||
| if err != nil { | ||
| grpcErr, ok := status.FromError(err) | ||
| if ok && grpcErr.Code() == codes.NotFound { | ||
| return &api.APIError{Code: http.StatusNotFound, ClientMsg: utils.SandboxNotFoundMsg(sbx.SandboxID), Err: err} | ||
| } | ||
|
|
||
| err = utils.UnwrapGRPCError(err) | ||
| telemetry.ReportCriticalError(ctx, "failed to update sandbox network on node", err) | ||
|
|
||
| return &api.APIError{ | ||
| Code: http.StatusInternalServerError, | ||
| ClientMsg: "Error applying network config to sandbox", | ||
| Err: fmt.Errorf("failed to update sandbox network on node: %w", err), | ||
| } | ||
| } | ||
|
|
||
| telemetry.ReportEvent(ctx, "Updated sandbox network on node") | ||
|
|
||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.