@@ -11,12 +11,13 @@ import (
1111 "strings"
1212 "time"
1313
14+ "github.com/asaskevich/govalidator"
1415 "github.com/gin-gonic/gin"
1516 "github.com/google/uuid"
1617 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
1718 "go.opentelemetry.io/otel/attribute"
1819 "go.opentelemetry.io/otel/trace"
19- "go.uber. org/zap "
20+ "golang. org/x/net/http/httpguts "
2021 "golang.org/x/net/idna"
2122
2223 "github.com/e2b-dev/infra/packages/api/internal/api"
@@ -32,7 +33,6 @@ import (
3233 "github.com/e2b-dev/infra/packages/shared/pkg/ginutils"
3334 "github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator"
3435 "github.com/e2b-dev/infra/packages/shared/pkg/id"
35- "github.com/e2b-dev/infra/packages/shared/pkg/logger"
3636 sbxlogger "github.com/e2b-dev/infra/packages/shared/pkg/logger/sandbox"
3737 "github.com/e2b-dev/infra/packages/shared/pkg/middleware/otel/metrics"
3838 sandbox_network "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network"
@@ -47,6 +47,13 @@ const (
4747
4848 // Network validation error messages
4949 ErrMsgDomainsRequireBlockAll = "When specifying allowed domains in allow out, you must include 'ALL_TRAFFIC' in deny out to block all other traffic."
50+
51+ maxNetworkRuleDomains = 10
52+ maxNetworkRuleTransformsPerDomain = 1
53+ maxNetworkRuleDomainLen = 128
54+ maxNetworkRuleHeaderNameLen = 64
55+ maxNetworkRuleHeaderValueLen = 2048
56+ maxNetworkRuleHeadersPerRule = 20
5057)
5158
5259func (a * APIStore ) PostSandboxes (c * gin.Context ) {
@@ -174,7 +181,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
174181
175182 var network * types.SandboxNetworkConfig
176183 if n := body .Network ; n != nil {
177- if err := validateNetworkConfig (n ); err != nil {
184+ if err := validateNetworkConfig (ctx , a . featureFlags , teamInfo . Team . ID , sharedUtils . DerefOrDefault ( build . EnvdVersion , "" ), n ); err != nil {
178185 telemetry .ReportError (ctx , "invalid network config" , err .Err , telemetry .WithSandboxID (sandboxID ))
179186 a .sendAPIStoreError (c , err .Code , err .ClientMsg )
180187
@@ -189,6 +196,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
189196 Egress : & types.SandboxNetworkEgressConfig {
190197 AllowedAddresses : sharedUtils .DerefOrDefault (n .AllowOut , nil ),
191198 DeniedAddresses : sharedUtils .DerefOrDefault (n .DenyOut , nil ),
199+ Rules : apiRulesToDBRules (n .Rules ),
192200 },
193201 }
194202
@@ -205,7 +213,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
205213 ctx , a .sqlcDB , a .featureFlags , teamInfo .ID , apiVolumeMounts , build ,
206214 )
207215 if err != nil {
208- if errors .Is (err , errVolumesNotSupported ) {
216+ if errors .Is (err , errVolumesNotSupported ) || errors . Is ( err , errNoEnvdVersion ) {
209217 a .sendAPIStoreError (c , http .StatusBadRequest , err .Error ())
210218
211219 return
@@ -265,6 +273,19 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
265273 return
266274 }
267275
276+ if n := body .Network ; n != nil && n .Rules != nil && len (* n .Rules ) > 0 {
277+ domains := make ([]string , 0 , len (* n .Rules ))
278+ for domain := range * n .Rules {
279+ domains = append (domains , domain )
280+ }
281+
282+ a .posthog .CreateAnalyticsTeamEvent (ctx , teamInfo .Team .ID .String (), "sandbox with network transform rules created" ,
283+ a .posthog .GetPackageToPosthogProperties (& c .Request .Header ).
284+ Set ("sandbox_id" , sandboxID ).
285+ Set ("domains" , domains ),
286+ )
287+ }
288+
268289 c .JSON (http .StatusCreated , & sbx )
269290}
270291
@@ -324,10 +345,35 @@ func (im InvalidVolumeMountsError) Error() string {
324345
325346var errVolumesNotSupported = errors .New ("volumes are not supported" )
326347
327- var errNoEnvdVersion = errors .New ("no envd version provided" )
348+ var errNetworkRulesNotSupported = errors .New ("network transform rules are not supported" )
349+
350+ var errNoEnvdVersion = errors .New ("template must be rebuilt: envd version is not set" )
351+
352+ const minEnvdVersionForNetworkRules = "0.5.13"
328353
329354const minEnvdVersionForVolumes = "0.5.14"
330355
356+ // checkEnvdVersionRequirement returns errNoEnvdVersion when buildVersion is empty, a parse
357+ // error when the version string is invalid, or a wrapped featureErr when the build does not
358+ // meet requiredMinVersion. The caller decides how to convert the returned error into an API
359+ // response so each call-site can produce its own status code / message.
360+ func checkEnvdVersionRequirement (buildVersion , requiredMinVersion string , featureErr error ) error {
361+ if buildVersion == "" {
362+ return errNoEnvdVersion
363+ }
364+
365+ ok , err := sharedUtils .IsGTEVersion (buildVersion , requiredMinVersion )
366+ if err != nil {
367+ return fmt .Errorf ("invalid envd version %q: %w" , buildVersion , err )
368+ }
369+
370+ if ! ok {
371+ return fmt .Errorf ("%w; template must be rebuilt. Template envd version is %s, must be at least %s" , featureErr , buildVersion , requiredMinVersion )
372+ }
373+
374+ return nil
375+ }
376+
331377func convertAPIVolumesToOrchestratorVolumes (ctx context.Context , sqlClient * sqlcdb.Client , featureFlags featureFlagsClient , teamID uuid.UUID , volumeMounts []api.SandboxVolumeMount , env * queries.EnvBuild ) ([]* orchestrator.SandboxVolumeMount , error ) {
332378 // are any volumes configured?
333379 if len (volumeMounts ) == 0 {
@@ -340,16 +386,9 @@ func convertAPIVolumesToOrchestratorVolumes(ctx context.Context, sqlClient *sqlc
340386 }
341387
342388 // does your envd version support volumes?
343- if envdVersion := sharedUtils .DerefOrDefault (env .EnvdVersion , "" ); envdVersion == "" {
344- logger .L ().Warn (ctx , "envd version is unset" )
345-
346- return nil , errNoEnvdVersion
347- } else if ok , err := sharedUtils .IsGTEVersion (envdVersion , minEnvdVersionForVolumes ); err != nil {
348- logger .L ().Warn (ctx , "failed to check envd version" , zap .Error (err ), zap .String ("envd_version" , envdVersion ))
349-
350- return nil , fmt .Errorf ("invalid envd version %q: %w" , envdVersion , err )
351- } else if ! ok {
352- return nil , fmt .Errorf ("%w; template must be rebuilt. Template envd version is %s, must be at least %s to support volumes" , errVolumesNotSupported , envdVersion , minEnvdVersionForVolumes )
389+ envdVersion := sharedUtils .DerefOrDefault (env .EnvdVersion , "" )
390+ if err := checkEnvdVersionRequirement (envdVersion , minEnvdVersionForVolumes , errVolumesNotSupported ); err != nil {
391+ return nil , err
353392 }
354393
355394 // get volumes from the database
@@ -514,7 +553,33 @@ func splitHostPortOptional(hostport string) (host string, port string, err error
514553 return host , port , nil
515554}
516555
517- func validateNetworkConfig (network * api.SandboxNetworkConfig ) * api.APIError {
556+ func apiRulesToDBRules (apiRules * map [string ][]api.SandboxNetworkRule ) map [string ][]types.SandboxNetworkRule {
557+ if apiRules == nil {
558+ return nil
559+ }
560+
561+ dbRules := make (map [string ][]types.SandboxNetworkRule , len (* apiRules ))
562+ for domain , rules := range * apiRules {
563+ dbDomainRules := make ([]types.SandboxNetworkRule , 0 , len (rules ))
564+ for _ , r := range rules {
565+ dbRule := types.SandboxNetworkRule {}
566+
567+ if r .Transform != nil {
568+ dbRule .Transform = & types.SandboxNetworkTransform {
569+ Headers : sharedUtils .DerefOrDefault (r .Transform .Headers , nil ),
570+ }
571+ }
572+
573+ dbDomainRules = append (dbDomainRules , dbRule )
574+ }
575+
576+ dbRules [domain ] = dbDomainRules
577+ }
578+
579+ return dbRules
580+ }
581+
582+ func validateNetworkConfig (ctx context.Context , featureFlags featureFlagsClient , teamID uuid.UUID , envdVersion string , network * api.SandboxNetworkConfig ) * api.APIError {
518583 if network == nil {
519584 return nil
520585 }
@@ -550,7 +615,11 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
550615 denyOut := sharedUtils .DerefOrDefault (network .DenyOut , nil )
551616 allowOut := sharedUtils .DerefOrDefault (network .AllowOut , nil )
552617
553- return validateEgressRules (allowOut , denyOut )
618+ if err := validateEgressRules (allowOut , denyOut ); err != nil {
619+ return err
620+ }
621+
622+ return validateNetworkRules (ctx , featureFlags , teamID , envdVersion , network .Rules )
554623}
555624
556625// validateEgressRules validates egress allow/deny rules:
@@ -593,3 +662,134 @@ func validateEgressRules(allowOut, denyOut []string) *api.APIError {
593662
594663 return nil
595664}
665+
666+ func validateNetworkRules (ctx context.Context , featureFlags featureFlagsClient , teamID uuid.UUID , envdVersion string , rules * map [string ][]api.SandboxNetworkRule ) * api.APIError {
667+ if rules == nil {
668+ return nil
669+ }
670+
671+ if ! featureFlags .BoolFlag (ctx , featureflags .NetworkTransformRulesFlag , featureflags .TeamContext (teamID .String ())) {
672+ return & api.APIError {
673+ Code : http .StatusBadRequest ,
674+ Err : fmt .Errorf ("team %s is not allowed to use network transform rules" , teamID ),
675+ ClientMsg : "Network transform rules are not available for your team." ,
676+ }
677+ }
678+
679+ if err := checkEnvdVersionRequirement (envdVersion , minEnvdVersionForNetworkRules , errNetworkRulesNotSupported ); err != nil {
680+ if errors .Is (err , errNetworkRulesNotSupported ) || errors .Is (err , errNoEnvdVersion ) {
681+ return & api.APIError {
682+ Code : http .StatusBadRequest ,
683+ Err : err ,
684+ ClientMsg : err .Error (),
685+ }
686+ }
687+
688+ return & api.APIError {
689+ Code : http .StatusInternalServerError ,
690+ Err : err ,
691+ ClientMsg : "internal error while validating network rules" ,
692+ }
693+ }
694+
695+ if len (* rules ) > maxNetworkRuleDomains {
696+ return & api.APIError {
697+ Code : http .StatusBadRequest ,
698+ Err : fmt .Errorf ("too many rule domains: %d (max %d)" , len (* rules ), maxNetworkRuleDomains ),
699+ ClientMsg : fmt .Sprintf ("Network rules can have at most %d domains." , maxNetworkRuleDomains ),
700+ }
701+ }
702+
703+ for domain , domainRules := range * rules {
704+ if len (domain ) == 0 {
705+ return & api.APIError {
706+ Code : http .StatusBadRequest ,
707+ Err : errors .New ("rule domain must not be empty" ),
708+ ClientMsg : "Rule domain must not be empty." ,
709+ }
710+ }
711+
712+ if len (domain ) > maxNetworkRuleDomainLen {
713+ return & api.APIError {
714+ Code : http .StatusBadRequest ,
715+ Err : fmt .Errorf ("rule domain %q exceeds max length %d" , domain , maxNetworkRuleDomainLen ),
716+ ClientMsg : fmt .Sprintf ("Rule domain %q exceeds maximum length of %d characters." , domain , maxNetworkRuleDomainLen ),
717+ }
718+ }
719+
720+ if ! govalidator .IsDNSName (domain ) {
721+ return & api.APIError {
722+ Code : http .StatusBadRequest ,
723+ Err : fmt .Errorf ("rule domain %q is not a valid domain" , domain ),
724+ ClientMsg : fmt .Sprintf ("Rule domain %q is not a valid domain name." , domain ),
725+ }
726+ }
727+
728+ if len (domainRules ) > maxNetworkRuleTransformsPerDomain {
729+ return & api.APIError {
730+ Code : http .StatusBadRequest ,
731+ Err : fmt .Errorf ("domain %q has %d transforms (max %d)" , domain , len (domainRules ), maxNetworkRuleTransformsPerDomain ),
732+ ClientMsg : fmt .Sprintf ("Domain %q can have at most %d transform rule." , domain , maxNetworkRuleTransformsPerDomain ),
733+ }
734+ }
735+
736+ for _ , rule := range domainRules {
737+ if rule .Transform == nil {
738+ continue
739+ }
740+
741+ headers := sharedUtils .DerefOrDefault (rule .Transform .Headers , nil )
742+ if len (headers ) > maxNetworkRuleHeadersPerRule {
743+ return & api.APIError {
744+ Code : http .StatusBadRequest ,
745+ Err : fmt .Errorf ("domain %q has %d headers (max %d)" , domain , len (headers ), maxNetworkRuleHeadersPerRule ),
746+ ClientMsg : fmt .Sprintf ("Domain %q can have at most %d headers per rule." , domain , maxNetworkRuleHeadersPerRule ),
747+ }
748+ }
749+
750+ for name , value := range headers {
751+ if len (name ) == 0 {
752+ return & api.APIError {
753+ Code : http .StatusBadRequest ,
754+ Err : fmt .Errorf ("header name in rule for domain %q must not be empty" , domain ),
755+ ClientMsg : fmt .Sprintf ("Header name in rule for domain %q must not be empty." , domain ),
756+ }
757+ }
758+
759+ if ! httpguts .ValidHeaderFieldName (name ) {
760+ return & api.APIError {
761+ Code : http .StatusBadRequest ,
762+ Err : fmt .Errorf ("header name %q in rule for domain %q contains invalid characters" , name , domain ),
763+ ClientMsg : fmt .Sprintf ("Header name %q in rule for domain %q must contain only valid HTTP token characters." , name , domain ),
764+ }
765+ }
766+
767+ if len (name ) > maxNetworkRuleHeaderNameLen {
768+ return & api.APIError {
769+ Code : http .StatusBadRequest ,
770+ Err : fmt .Errorf ("header name %q in rule for domain %q exceeds max length %d" , name , domain , maxNetworkRuleHeaderNameLen ),
771+ ClientMsg : fmt .Sprintf ("Header name %q in rule for domain %q exceeds maximum length of %d characters." , name , domain , maxNetworkRuleHeaderNameLen ),
772+ }
773+ }
774+
775+ if ! httpguts .ValidHeaderFieldValue (value ) {
776+ return & api.APIError {
777+ Code : http .StatusBadRequest ,
778+ Err : fmt .Errorf ("value for header %q in rule for domain %q contains invalid characters" , name , domain ),
779+ ClientMsg : fmt .Sprintf ("Value for header %q in rule for domain %q contains invalid characters." , name , domain ),
780+ }
781+ }
782+
783+ if len (value ) > maxNetworkRuleHeaderValueLen {
784+ return & api.APIError {
785+ Code : http .StatusBadRequest ,
786+ Err : fmt .Errorf ("value for header %q in rule for domain %q exceeds max length %d" , name , domain , maxNetworkRuleHeaderValueLen ),
787+ ClientMsg : fmt .Sprintf ("Value for header %q in rule for domain %q exceeds maximum length of %d characters." , name , domain , maxNetworkRuleHeaderValueLen ),
788+ }
789+ }
790+ }
791+ }
792+ }
793+
794+ return nil
795+ }
0 commit comments