Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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.

347 changes: 185 additions & 162 deletions packages/api/internal/api/api.gen.go

Large diffs are not rendered by default.

173 changes: 170 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,13 @@ 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 = 2048
maxNetworkRuleHeadersPerRule = 20
)

func (a *APIStore) PostSandboxes(c *gin.Context) {
Expand Down Expand Up @@ -174,7 +182,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 +197,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 @@ -265,6 +274,19 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
return
}

if n := body.Network; n != nil && n.Rules != nil && len(*n.Rules) > 0 {
domains := make([]string, 0, len(*n.Rules))
for domain := range *n.Rules {
domains = append(domains, domain)
}

a.posthog.CreateAnalyticsTeamEvent(ctx, teamInfo.Team.ID.String(), "sandbox with network transform rules created",
a.posthog.GetPackageToPosthogProperties(&c.Request.Header).
Set("sandbox_id", sandboxID).
Set("domains", domains),
)
}

c.JSON(http.StatusCreated, &sbx)
}

Expand Down Expand Up @@ -514,7 +536,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 +598,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 +645,118 @@ 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)
if len(headers) > maxNetworkRuleHeadersPerRule {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("domain %q has %d headers (max %d)", domain, len(headers), maxNetworkRuleHeadersPerRule),
ClientMsg: fmt.Sprintf("Domain %q can have at most %d headers per rule.", domain, maxNetworkRuleHeadersPerRule),
}
}

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 strings.ContainsAny(name, "\r\n") {
Comment thread
sitole marked this conversation as resolved.
Outdated
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("header name %q in rule for domain %q contains invalid characters", name, domain),
ClientMsg: fmt.Sprintf("Header name %q in rule for domain %q must not contain CR or LF characters.", name, domain),
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
sitole marked this conversation as resolved.

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 strings.ContainsAny(value, "\r\n") {
Comment thread
sitole marked this conversation as resolved.
Outdated
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("value for header %q in rule for domain %q contains invalid characters", name, domain),
ClientMsg: fmt.Sprintf("Value for header %q in rule for domain %q must not contain CR or LF characters.", name, domain),
}
}

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