Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bdd1fac
Openapi for network rules
sitole Apr 28, 2026
b25dc55
Orchestrator gRPC for network rules
sitole Apr 28, 2026
8569059
Bump orchestrator verions with network rules support
sitole Apr 28, 2026
6627f22
Validate and propagate sbx network rules to orchestrator
sitole Apr 28, 2026
10844a3
Use static domain validator
sitole Apr 28, 2026
bdf8920
Tests for network rules object
sitole Apr 28, 2026
084caa7
chore: auto-commit generated changes
github-actions[bot] Apr 28, 2026
66ce87e
Posthog events
sitole Apr 28, 2026
192fc67
Update comment about domain patterns in openapi
sitole Apr 28, 2026
202ebb6
api: reject header names/values containing CR or LF characters
sitole Apr 28, 2026
8bb62c9
api: clone headers map in dbNetworkConfigToAPI
sitole Apr 28, 2026
4c96456
api: fix posthog event label in network update handler
sitole Apr 28, 2026
f88af9e
Merge branch 'main' into feat/api-for-setting-up-domain-transform-eng…
sitole Apr 29, 2026
5db6f5c
api/handlers: tighten network rule header limits
sitole Apr 29, 2026
64dc39a
api/orchestrator: keep Rules nil in egress config when no rules given
sitole Apr 29, 2026
3917154
Use native tool to validate header name
sitole Apr 29, 2026
5830584
fix inverted if
sitole Apr 29, 2026
dc81689
Fixed error message for invalid header name format
sitole Apr 30, 2026
78b77c6
Transform rules can only apply to template builds with supported envd…
sitole Apr 30, 2026
a0fe0c0
Use `httpguts.ValidHeaderFieldValue` to validate transform header values
sitole Apr 30, 2026
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
1 change: 1 addition & 0 deletions packages/api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tool (
)

require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bsm/redislock v0.9.4
github.com/caarlos0/env/v11 v11.3.1
github.com/e2b-dev/infra/packages/auth v0.0.0
Expand Down
2 changes: 2 additions & 0 deletions packages/api/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

348 changes: 186 additions & 162 deletions packages/api/internal/api/api.gen.go

Large diffs are not rendered by default.

135 changes: 132 additions & 3 deletions packages/api/internal/handlers/sandbox_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"time"

"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
Expand Down Expand Up @@ -47,6 +48,12 @@ const (

// Network validation error messages
ErrMsgDomainsRequireBlockAll = "When specifying allowed domains in allow out, you must include 'ALL_TRAFFIC' in deny out to block all other traffic."

maxNetworkRuleDomains = 10
maxNetworkRuleTransformsPerDomain = 1
maxNetworkRuleDomainLen = 128
maxNetworkRuleHeaderNameLen = 64
Comment thread
sitole marked this conversation as resolved.
maxNetworkRuleHeaderValueLen = 256
Comment thread
sitole marked this conversation as resolved.
Outdated
)

func (a *APIStore) PostSandboxes(c *gin.Context) {
Expand Down Expand Up @@ -174,7 +181,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {

var network *types.SandboxNetworkConfig
if n := body.Network; n != nil {
if err := validateNetworkConfig(n); err != nil {
if err := validateNetworkConfig(ctx, a.featureFlags, teamInfo.Team.ID, n); err != nil {
telemetry.ReportError(ctx, "invalid network config", err.Err, telemetry.WithSandboxID(sandboxID))
a.sendAPIStoreError(c, err.Code, err.ClientMsg)

Expand All @@ -189,6 +196,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
Egress: &types.SandboxNetworkEgressConfig{
AllowedAddresses: sharedUtils.DerefOrDefault(n.AllowOut, nil),
DeniedAddresses: sharedUtils.DerefOrDefault(n.DenyOut, nil),
Rules: apiRulesToDBRules(n.Rules),
},
}

Expand Down Expand Up @@ -514,7 +522,33 @@ func splitHostPortOptional(hostport string) (host string, port string, err error
return host, port, nil
}

func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
func apiRulesToDBRules(apiRules *map[string][]api.SandboxNetworkRule) map[string][]types.SandboxNetworkRule {
if apiRules == nil {
return nil
}

dbRules := make(map[string][]types.SandboxNetworkRule, len(*apiRules))
for domain, rules := range *apiRules {
dbDomainRules := make([]types.SandboxNetworkRule, 0, len(rules))
for _, r := range rules {
dbRule := types.SandboxNetworkRule{}

if r.Transform != nil {
dbRule.Transform = &types.SandboxNetworkTransform{
Headers: sharedUtils.DerefOrDefault(r.Transform.Headers, nil),
}
}

dbDomainRules = append(dbDomainRules, dbRule)
}

dbRules[domain] = dbDomainRules
}

return dbRules
}

func validateNetworkConfig(ctx context.Context, featureFlags featureFlagsClient, teamID uuid.UUID, network *api.SandboxNetworkConfig) *api.APIError {
if network == nil {
return nil
}
Expand Down Expand Up @@ -550,7 +584,11 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
denyOut := sharedUtils.DerefOrDefault(network.DenyOut, nil)
allowOut := sharedUtils.DerefOrDefault(network.AllowOut, nil)

return validateEgressRules(allowOut, denyOut)
if err := validateEgressRules(allowOut, denyOut); err != nil {
return err
}

return validateNetworkRules(ctx, featureFlags, teamID, network.Rules)
Comment thread
sitole marked this conversation as resolved.
Outdated
}

// validateEgressRules validates egress allow/deny rules:
Expand Down Expand Up @@ -593,3 +631,94 @@ func validateEgressRules(allowOut, denyOut []string) *api.APIError {

return nil
}

func validateNetworkRules(ctx context.Context, featureFlags featureFlagsClient, teamID uuid.UUID, rules *map[string][]api.SandboxNetworkRule) *api.APIError {
if rules == nil {
return nil
}

if !featureFlags.BoolFlag(ctx, featureflags.NetworkTransformRulesFlag, featureflags.TeamContext(teamID.String())) {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("team %s is not allowed to use network transform rules", teamID),
ClientMsg: "Network transform rules are not available for your team.",
}
}

if len(*rules) > maxNetworkRuleDomains {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("too many rule domains: %d (max %d)", len(*rules), maxNetworkRuleDomains),
ClientMsg: fmt.Sprintf("Network rules can have at most %d domains.", maxNetworkRuleDomains),
}
}

for domain, domainRules := range *rules {
if len(domain) == 0 {
return &api.APIError{
Code: http.StatusBadRequest,
Err: errors.New("rule domain must not be empty"),
ClientMsg: "Rule domain must not be empty.",
}
}

if len(domain) > maxNetworkRuleDomainLen {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("rule domain %q exceeds max length %d", domain, maxNetworkRuleDomainLen),
ClientMsg: fmt.Sprintf("Rule domain %q exceeds maximum length of %d characters.", domain, maxNetworkRuleDomainLen),
}
}

if !govalidator.IsDNSName(domain) {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("rule domain %q is not a valid domain", domain),
ClientMsg: fmt.Sprintf("Rule domain %q is not a valid domain name.", domain),
}
}

if len(domainRules) > maxNetworkRuleTransformsPerDomain {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("domain %q has %d transforms (max %d)", domain, len(domainRules), maxNetworkRuleTransformsPerDomain),
ClientMsg: fmt.Sprintf("Domain %q can have at most %d transform rule.", domain, maxNetworkRuleTransformsPerDomain),
}
}

for _, rule := range domainRules {
if rule.Transform == nil {
continue
}

headers := sharedUtils.DerefOrDefault(rule.Transform.Headers, nil)
for name, value := range headers {
if len(name) == 0 {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("header name in rule for domain %q must not be empty", domain),
ClientMsg: fmt.Sprintf("Header name in rule for domain %q must not be empty.", domain),
}
}

if len(name) > maxNetworkRuleHeaderNameLen {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("header name %q in rule for domain %q exceeds max length %d", name, domain, maxNetworkRuleHeaderNameLen),
ClientMsg: fmt.Sprintf("Header name %q in rule for domain %q exceeds maximum length of %d characters.", name, domain, maxNetworkRuleHeaderNameLen),
}
}

if len(value) > maxNetworkRuleHeaderValueLen {
Comment thread
sitole marked this conversation as resolved.
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("value for header %q in rule for domain %q exceeds max length %d", name, domain, maxNetworkRuleHeaderValueLen),
ClientMsg: fmt.Sprintf("Value for header %q in rule for domain %q exceeds maximum length of %d characters.", name, domain, maxNetworkRuleHeaderValueLen),
}
}
}
}
}

return nil
}
Loading
Loading