Skip to content

Commit 0f24c07

Browse files
committed
Merge branch 'main' of github.com:e2b-dev/infra into lev-ingress-control
Incorporate main commits: #2150 (validate specified IPs in egress), #2156 (disable request timeout), #2154 (envd init logs), #2151 (storage cache test fix). Resolve conflict in sandbox_create.go by consolidating validation: Egress validation — matches main #2150 + port rejection: - validateEgressRules stays in sandbox_create.go (same location as main) - Uses IsSpecifiedIPOrCIDR from #2150 to reject unspecified addresses (0.0.0.0, ::, 0.0.0.0/24, etc.) while allowing 0.0.0.0/0 - Uses ParseAddressesAndDomains to separate IPs from domains (same as main) - Adds port rejection (egress doesn't support port-specific rules) Ingress validation — new, added next to validateEgressRules: - validateIngressRules + validateIngressEntry in sandbox_create.go - Uses SplitHostPort to separate CIDR from port, validates each part - Uses IsSpecifiedIPOrCIDR for IPv4 + allows ::/0 for IPv6 block-all - Uses ParsePortRange for port/port-range validation - Rejects domains (ingress is IP/CIDR only) - Requires deny-all when allow rules are present Simplify rule.go — remove ParseRule/ParseRules: - Egress validation uses IsSpecifiedIPOrCIDR + ParseAddressesAndDomains directly - Ingress validation uses SplitHostPort + IsIPOrCIDR + ParsePortRange directly - Keep: Rule/ACL structs (hot-path matching), SplitHostPort, ParsePortRange (public) make[1]: Entering directory '/home/lev/dev/infra/iac/provider-gcp' - Remove: ParseRule, ParseRules (tried to be one-size-fits-all, added complexity) Simplify orchestrator ACL building: - newEgressACL: uses parseCIDRs (direct net.ParseCIDR, no ParseRules) - newIngressACL/parseIngressRules: unchanged (builds from proto fields) Update create_instance.go: - buildEgressConfig: uses IsIPOrCIDR for domain detection (was ParseRule) - parseIngressRules: uses SplitHostPort + ParsePortRange (was ParseRule) Test updates: - Error messages updated to match main's IsSpecifiedIPOrCIDR style - Integration tests: replace ::/1/0.0.0.0/1 with ::/0/0.0.0.0/0 (unspecified network addresses now rejected) - TestIsSpecifiedIPOrCIDR preserved from #2150 - TestSplitHostPort, TestParsePortRange replace TestParseRule/TestParseRules
2 parents e363fb6 + e236dcc commit 0f24c07

15 files changed

Lines changed: 368 additions & 527 deletions

File tree

packages/api/internal/handlers/sandbox_create.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"net/http"
99
"path/filepath"
10+
"slices"
1011
"strings"
1112
"time"
1213

@@ -31,6 +32,7 @@ import (
3132
"github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator"
3233
"github.com/e2b-dev/infra/packages/shared/pkg/id"
3334
sbxlogger "github.com/e2b-dev/infra/packages/shared/pkg/logger/sandbox"
35+
sandboxnetwork "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network"
3436
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
3537
sharedUtils "github.com/e2b-dev/infra/packages/shared/pkg/utils"
3638
)
@@ -514,3 +516,129 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
514516

515517
return validateIngressRules(allowIn, denyIn)
516518
}
519+
520+
// validateEgressRules validates egress allow/deny rules:
521+
// - denyOut entries must be specified IPs or CIDRs (not domains, no ports)
522+
// - allowOut entries can be specified IPs, CIDRs, or domain names (no ports)
523+
// - when allowOut contains domains, denyOut must include 0.0.0.0/0
524+
func validateEgressRules(allowOut, denyOut []string) *api.APIError {
525+
for _, entry := range denyOut {
526+
host, port, _ := sandboxnetwork.SplitHostPort(entry)
527+
if port != "" {
528+
return &api.APIError{
529+
Code: http.StatusBadRequest,
530+
Err: fmt.Errorf("invalid deny out entry %q: port-specific rules are not supported for egress", entry),
531+
ClientMsg: fmt.Sprintf("invalid deny out entry %q: port-specific rules are not supported for egress", entry),
532+
}
533+
}
534+
535+
if !sandboxnetwork.IsSpecifiedIPOrCIDR(host) {
536+
return &api.APIError{
537+
Code: http.StatusBadRequest,
538+
Err: fmt.Errorf("invalid denied CIDR %s", host),
539+
ClientMsg: fmt.Sprintf("invalid denied CIDR %s", host),
540+
}
541+
}
542+
}
543+
544+
hasDomains := false
545+
for _, entry := range allowOut {
546+
host, port, _ := sandboxnetwork.SplitHostPort(entry)
547+
if port != "" {
548+
return &api.APIError{
549+
Code: http.StatusBadRequest,
550+
Err: fmt.Errorf("invalid allow out entry %q: port-specific rules are not supported for egress", entry),
551+
ClientMsg: fmt.Sprintf("invalid allow out entry %q: port-specific rules are not supported for egress", entry),
552+
}
553+
}
554+
555+
if sandboxnetwork.IsIPOrCIDR(host) {
556+
if !sandboxnetwork.IsSpecifiedIPOrCIDR(host) {
557+
return &api.APIError{
558+
Code: http.StatusBadRequest,
559+
Err: fmt.Errorf("invalid allowed address %s", host),
560+
ClientMsg: fmt.Sprintf("invalid allowed address %s", host),
561+
}
562+
}
563+
} else {
564+
hasDomains = true
565+
}
566+
}
567+
568+
if hasDomains {
569+
hasBlockAll := slices.Contains(denyOut, sandboxnetwork.AllInternetTrafficCIDR)
570+
if !hasBlockAll {
571+
return &api.APIError{
572+
Code: http.StatusBadRequest,
573+
Err: fmt.Errorf("allow out contains domains but deny out is missing 0.0.0.0/0 (ALL_TRAFFIC)"),
574+
ClientMsg: ErrMsgDomainsRequireBlockAll,
575+
}
576+
}
577+
}
578+
579+
return nil
580+
}
581+
582+
// validateIngressRules validates ingress allow/deny rules:
583+
// - entries must be valid IP or CIDR with optional port/port-range (no domains)
584+
// - IPv6 is supported (including ::/0)
585+
// - when allowIn is set, denyIn must include 0.0.0.0/0
586+
func validateIngressRules(allowIn, denyIn []string) *api.APIError {
587+
for _, entry := range denyIn {
588+
if err := validateIngressEntry(entry); err != nil {
589+
return &api.APIError{
590+
Code: http.StatusBadRequest,
591+
Err: fmt.Errorf("invalid deny in entry %q: %w", entry, err),
592+
ClientMsg: fmt.Sprintf("invalid deny in entry %q: %s", entry, err),
593+
}
594+
}
595+
}
596+
597+
for _, entry := range allowIn {
598+
if err := validateIngressEntry(entry); err != nil {
599+
return &api.APIError{
600+
Code: http.StatusBadRequest,
601+
Err: fmt.Errorf("invalid allow in entry %q: %w", entry, err),
602+
ClientMsg: fmt.Sprintf("invalid allow in entry %q: %s", entry, err),
603+
}
604+
}
605+
}
606+
607+
if len(allowIn) > 0 {
608+
hasBlockAll := slices.Contains(denyIn, sandboxnetwork.AllInternetTrafficCIDR)
609+
if !hasBlockAll {
610+
return &api.APIError{
611+
Code: http.StatusBadRequest,
612+
Err: fmt.Errorf("allow in requires deny in to include 0.0.0.0/0 (ALL_TRAFFIC)"),
613+
ClientMsg: ErrMsgAllowInRequiresBlockAll,
614+
}
615+
}
616+
}
617+
618+
return nil
619+
}
620+
621+
// validateIngressEntry validates a single ingress rule entry (CIDR[:port]).
622+
func validateIngressEntry(entry string) error {
623+
host, portStr, err := sandboxnetwork.SplitHostPort(entry)
624+
if err != nil {
625+
return err
626+
}
627+
628+
if !sandboxnetwork.IsIPOrCIDR(host) {
629+
return fmt.Errorf("domains are not supported for ingress rules")
630+
}
631+
632+
// IsSpecifiedIPOrCIDR allows 0.0.0.0/0 but not ::/0; ingress needs both.
633+
if !sandboxnetwork.IsSpecifiedIPOrCIDR(host) && host != "::/0" {
634+
return fmt.Errorf("unspecified address")
635+
}
636+
637+
if portStr != "" {
638+
if _, _, err := sandboxnetwork.ParsePortRange(portStr); err != nil {
639+
return err
640+
}
641+
}
642+
643+
return nil
644+
}

packages/api/internal/handlers/sandbox_create_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestValidateNetworkConfig(t *testing.T) {
104104
},
105105
wantErr: true,
106106
wantCode: http.StatusBadRequest,
107-
wantErrMsg: `invalid deny out entry "not-a-cidr": domains are not supported in deny rules`,
107+
wantErrMsg: `invalid denied CIDR not-a-cidr`,
108108
},
109109
// Port syntax rejected for egress
110110
{
@@ -114,7 +114,7 @@ func TestValidateNetworkConfig(t *testing.T) {
114114
},
115115
wantErr: true,
116116
wantCode: http.StatusBadRequest,
117-
wantErrMsg: `invalid deny out entry "10.0.0.0/8": port-specific rules are not supported for egress`,
117+
wantErrMsg: `invalid deny out entry "10.0.0.0/8:22": port-specific rules are not supported for egress`,
118118
},
119119
{
120120
name: "allow_out with port is rejected",
@@ -123,7 +123,7 @@ func TestValidateNetworkConfig(t *testing.T) {
123123
},
124124
wantErr: true,
125125
wantCode: http.StatusBadRequest,
126-
wantErrMsg: `invalid allow out entry "8.8.8.8": port-specific rules are not supported for egress`,
126+
wantErrMsg: `invalid allow out entry "8.8.8.8:80": port-specific rules are not supported for egress`,
127127
},
128128
// Domain validation tests
129129
{
@@ -370,7 +370,7 @@ func TestValidateNetworkConfig(t *testing.T) {
370370
},
371371
wantErr: true,
372372
wantCode: http.StatusBadRequest,
373-
wantErrMsg: `invalid allow out entry "8.8.8.8": port-specific rules are not supported for egress`,
373+
wantErrMsg: `invalid allow out entry "8.8.8.8:80": port-specific rules are not supported for egress`,
374374
},
375375
{
376376
name: "deny_out with port is rejected",
@@ -379,7 +379,7 @@ func TestValidateNetworkConfig(t *testing.T) {
379379
},
380380
wantErr: true,
381381
wantCode: http.StatusBadRequest,
382-
wantErrMsg: `invalid deny out entry "10.0.0.0/8": port-specific rules are not supported for egress`,
382+
wantErrMsg: `invalid deny out entry "10.0.0.0/8:22": port-specific rules are not supported for egress`,
383383
},
384384
{
385385
name: "deny_out with domain is rejected",
@@ -388,7 +388,7 @@ func TestValidateNetworkConfig(t *testing.T) {
388388
},
389389
wantErr: true,
390390
wantCode: http.StatusBadRequest,
391-
wantErrMsg: `invalid deny out entry "example.com": domains are not supported in deny rules`,
391+
wantErrMsg: `invalid denied CIDR example.com`,
392392
},
393393
{
394394
name: "deny_out with invalid port is rejected",
@@ -397,7 +397,7 @@ func TestValidateNetworkConfig(t *testing.T) {
397397
},
398398
wantErr: true,
399399
wantCode: http.StatusBadRequest,
400-
wantErrMsg: `invalid deny out entry: invalid entry "10.0.0.0/8:abc": invalid port "abc": strconv.ParseUint: parsing "abc": invalid syntax`,
400+
wantErrMsg: `invalid deny out entry "10.0.0.0/8:abc": port-specific rules are not supported for egress`,
401401
},
402402
}
403403

packages/api/internal/handlers/sandbox_network_update.go

Lines changed: 0 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ package handlers
33
import (
44
"fmt"
55
"net/http"
6-
"slices"
76

87
"github.com/gin-gonic/gin"
98

109
"github.com/e2b-dev/infra/packages/api/internal/api"
1110
"github.com/e2b-dev/infra/packages/api/internal/utils"
1211
"github.com/e2b-dev/infra/packages/auth/pkg/auth"
1312
"github.com/e2b-dev/infra/packages/db/pkg/types"
14-
sandboxnetwork "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network"
1513
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
1614
sharedutils "github.com/e2b-dev/infra/packages/shared/pkg/utils"
1715
)
@@ -72,133 +70,3 @@ func (a *APIStore) PutSandboxesSandboxIDNetwork(
7270

7371
c.Status(http.StatusNoContent)
7472
}
75-
76-
// validateEgressRules validates egress allow/deny rules:
77-
// - denyOut entries must be IPs or CIDRs (not domains, no ports)
78-
// - allowOut entries can be IPs, CIDRs, or domain names (no ports)
79-
// - when allowOut contains domains, denyOut must include 0.0.0.0/0
80-
func validateEgressRules(allowOut, denyOut []string) *api.APIError {
81-
denyRules, err := sandboxnetwork.ParseRules(denyOut)
82-
if err != nil {
83-
return &api.APIError{
84-
Code: http.StatusBadRequest,
85-
Err: fmt.Errorf("invalid deny out entry: %w", err),
86-
ClientMsg: fmt.Sprintf("invalid deny out entry: %s", err),
87-
}
88-
}
89-
90-
for _, rule := range denyRules {
91-
if rule.IsDomain {
92-
return &api.APIError{
93-
Code: http.StatusBadRequest,
94-
Err: fmt.Errorf("invalid deny out entry %q: domains are not supported in deny rules", rule.Host),
95-
ClientMsg: fmt.Sprintf("invalid deny out entry %q: domains are not supported in deny rules", rule.Host),
96-
}
97-
}
98-
99-
if rule.HasPort() {
100-
return &api.APIError{
101-
Code: http.StatusBadRequest,
102-
Err: fmt.Errorf("invalid deny out entry %q: port-specific rules are not supported for egress", rule.Host),
103-
ClientMsg: fmt.Sprintf("invalid deny out entry %q: port-specific rules are not supported for egress", rule.Host),
104-
}
105-
}
106-
}
107-
108-
allowRules, err := sandboxnetwork.ParseRules(allowOut)
109-
if err != nil {
110-
return &api.APIError{
111-
Code: http.StatusBadRequest,
112-
Err: fmt.Errorf("invalid allow out entry: %w", err),
113-
ClientMsg: fmt.Sprintf("invalid allow out entry: %s", err),
114-
}
115-
}
116-
117-
hasDomains := false
118-
for _, rule := range allowRules {
119-
if rule.IsDomain {
120-
hasDomains = true
121-
} else if rule.HasPort() {
122-
return &api.APIError{
123-
Code: http.StatusBadRequest,
124-
Err: fmt.Errorf("invalid allow out entry %q: port-specific rules are not supported for egress", rule.Host),
125-
ClientMsg: fmt.Sprintf("invalid allow out entry %q: port-specific rules are not supported for egress", rule.Host),
126-
}
127-
}
128-
}
129-
130-
if hasDomains {
131-
hasBlockAll := slices.ContainsFunc(denyRules, func(r sandboxnetwork.Rule) bool {
132-
return r.Host == sandboxnetwork.AllInternetTrafficCIDR && r.AllPorts()
133-
})
134-
135-
if !hasBlockAll {
136-
return &api.APIError{
137-
Code: http.StatusBadRequest,
138-
Err: fmt.Errorf("allow out contains domains but deny out is missing 0.0.0.0/0 (ALL_TRAFFIC)"),
139-
ClientMsg: ErrMsgDomainsRequireBlockAll,
140-
}
141-
}
142-
}
143-
144-
return nil
145-
}
146-
147-
// validateIngressRules validates ingress allow/deny rules:
148-
// - entries must be valid CIDR[:port] strings (no domains)
149-
// - when allowIn is set, denyIn must include 0.0.0.0/0
150-
func validateIngressRules(allowIn, denyIn []string) *api.APIError {
151-
denyRules, err := sandboxnetwork.ParseRules(denyIn)
152-
if err != nil {
153-
return &api.APIError{
154-
Code: http.StatusBadRequest,
155-
Err: fmt.Errorf("invalid deny in entry: %w", err),
156-
ClientMsg: fmt.Sprintf("invalid deny in entry: %s", err),
157-
}
158-
}
159-
160-
for _, rule := range denyRules {
161-
if rule.IsDomain {
162-
return &api.APIError{
163-
Code: http.StatusBadRequest,
164-
Err: fmt.Errorf("invalid deny in entry %q: domains are not supported for ingress rules", rule.Host),
165-
ClientMsg: fmt.Sprintf("invalid deny in entry %q: domains are not supported for ingress rules", rule.Host),
166-
}
167-
}
168-
}
169-
170-
allowRules, err := sandboxnetwork.ParseRules(allowIn)
171-
if err != nil {
172-
return &api.APIError{
173-
Code: http.StatusBadRequest,
174-
Err: fmt.Errorf("invalid allow in entry: %w", err),
175-
ClientMsg: fmt.Sprintf("invalid allow in entry: %s", err),
176-
}
177-
}
178-
179-
for _, rule := range allowRules {
180-
if rule.IsDomain {
181-
return &api.APIError{
182-
Code: http.StatusBadRequest,
183-
Err: fmt.Errorf("invalid allow in entry %q: domains are not supported for ingress rules", rule.Host),
184-
ClientMsg: fmt.Sprintf("invalid allow in entry %q: domains are not supported for ingress rules", rule.Host),
185-
}
186-
}
187-
}
188-
189-
if len(allowRules) > 0 {
190-
hasBlockAll := slices.ContainsFunc(denyRules, func(r sandboxnetwork.Rule) bool {
191-
return r.Host == sandboxnetwork.AllInternetTrafficCIDR && r.AllPorts()
192-
})
193-
194-
if !hasBlockAll {
195-
return &api.APIError{
196-
Code: http.StatusBadRequest,
197-
Err: fmt.Errorf("allow in requires deny in to include 0.0.0.0/0 (ALL_TRAFFIC)"),
198-
ClientMsg: ErrMsgAllowInRequiresBlockAll,
199-
}
200-
}
201-
}
202-
203-
return nil
204-
}

0 commit comments

Comments
 (0)