diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index 5853823862..016e1c8c66 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -714,15 +714,27 @@ type SandboxMetric struct { // SandboxNetworkConfig defines model for SandboxNetworkConfig. type SandboxNetworkConfig struct { + // AllowIn List of client IP CIDRs allowed to reach the sandbox (e.g. "203.0.113.0/24"). If set, only matching clients can connect. Allowed entries always take precedence over denied entries. + AllowIn *[]string `json:"allowIn,omitempty"` + // AllowOut List of allowed destinations for egress traffic. Each entry can be a CIDR block (e.g. "8.8.8.8/32"), a bare IP address (e.g. "8.8.8.8"), or a domain name (e.g. "example.com", "*.example.com"). Allowed entries always take precedence over denied entries. AllowOut *[]string `json:"allowOut,omitempty"` + // AllowPorts List of ports accessible from outside the sandbox. If set, only these ports accept inbound HTTP connections. Allowed ports always take precedence over denied ports. + AllowPorts *[]int `json:"allowPorts,omitempty"` + // AllowPublicTraffic Specify if the sandbox URLs should be accessible only with authentication. AllowPublicTraffic *bool `json:"allowPublicTraffic,omitempty"` + // DenyIn List of client IP CIDRs denied from reaching the sandbox. + DenyIn *[]string `json:"denyIn,omitempty"` + // DenyOut List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. DenyOut *[]string `json:"denyOut,omitempty"` + // DenyPorts List of ports blocked from outside access. Ignored if allowPorts is set. + DenyPorts *[]int `json:"denyPorts,omitempty"` + // MaskRequestHost Specify host mask which will be used for all sandbox requests MaskRequestHost *string `json:"maskRequestHost,omitempty"` } @@ -1267,11 +1279,26 @@ type GetSandboxesSandboxIDMetricsParams struct { // PutSandboxesSandboxIDNetworkJSONBody defines parameters for PutSandboxesSandboxIDNetwork. type PutSandboxesSandboxIDNetworkJSONBody struct { + // AllowIn List of client IP CIDRs allowed to reach the sandbox (e.g. "203.0.113.0/24"). If set, only matching clients can connect. Allowed entries always take precedence over denied entries. + AllowIn *[]string `json:"allowIn,omitempty"` + // AllowOut List of allowed destinations for egress traffic. Each entry can be a CIDR block (e.g. "8.8.8.8/32"), a bare IP address (e.g. "8.8.8.8"), or a domain name (e.g. "example.com", "*.example.com"). Allowed entries always take precedence over denied entries. AllowOut *[]string `json:"allowOut,omitempty"` + // AllowPorts List of ports accessible from outside the sandbox. If set, only these ports accept inbound HTTP connections. Allowed ports always take precedence over denied ports. + AllowPorts *[]int `json:"allowPorts,omitempty"` + + // DenyIn List of client IP CIDRs denied from reaching the sandbox. + DenyIn *[]string `json:"denyIn,omitempty"` + // DenyOut List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. DenyOut *[]string `json:"denyOut,omitempty"` + + // DenyPorts List of ports blocked from outside access. Ignored if allowPorts is set. + DenyPorts *[]int `json:"denyPorts,omitempty"` + + // MaskRequestHost Specify host mask which will be used for all sandbox requests. Set to null to clear. + MaskRequestHost *string `json:"maskRequestHost"` } // PostSandboxesSandboxIDRefreshesJSONBody defines parameters for PostSandboxesSandboxIDRefreshes. @@ -12080,162 +12107,165 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "dQuLym/aI1v8XRzc14//PwAA//8TnG3kTyIBAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/api/internal/handlers/sandbox_create.go b/packages/api/internal/handlers/sandbox_create.go index 004ddd69a2..1b6bc082fe 100644 --- a/packages/api/internal/handlers/sandbox_create.go +++ b/packages/api/internal/handlers/sandbox_create.go @@ -7,7 +7,6 @@ import ( "net" "net/http" "path/filepath" - "slices" "strings" "time" @@ -32,7 +31,6 @@ import ( "github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator" "github.com/e2b-dev/infra/packages/shared/pkg/id" sbxlogger "github.com/e2b-dev/infra/packages/shared/pkg/logger/sandbox" - sandbox_network "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network" "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" sharedUtils "github.com/e2b-dev/infra/packages/shared/pkg/utils" ) @@ -165,8 +163,12 @@ func (a *APIStore) PostSandboxes(c *gin.Context) { network = &types.SandboxNetworkConfig{ Ingress: &types.SandboxNetworkIngressConfig{ - AllowPublicAccess: n.AllowPublicTraffic, - MaskRequestHost: n.MaskRequestHost, + AllowPublicAccess: n.AllowPublicTraffic, + MaskRequestHost: n.MaskRequestHost, + AllowedPorts: intsToUint32s(n.AllowPorts), + DeniedPorts: intsToUint32s(n.DenyPorts), + AllowedClientCIDRs: sharedUtils.DerefOrDefault(n.AllowIn, nil), + DeniedClientCIDRs: sharedUtils.DerefOrDefault(n.DenyIn, nil), }, Egress: &types.SandboxNetworkEgressConfig{ AllowedAddresses: sharedUtils.DerefOrDefault(n.AllowOut, nil), @@ -504,36 +506,14 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError { denyOut := sharedUtils.DerefOrDefault(network.DenyOut, nil) allowOut := sharedUtils.DerefOrDefault(network.AllowOut, nil) - return validateEgressRules(allowOut, denyOut) -} - -// validateEgressRules validates egress allow/deny rules: -// - denyOut entries must be valid IPs or CIDRs (not domains) -// - allowOut entries must be valid IPs, CIDRs, or domain names -// - when allowOut contains domains, denyOut must include 0.0.0.0/0 -func validateEgressRules(allowOut, denyOut []string) *api.APIError { - for _, cidr := range denyOut { - if !sandbox_network.IsIPOrCIDR(cidr) { - return &api.APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("invalid denied CIDR %s", cidr), - ClientMsg: fmt.Sprintf("invalid denied CIDR %s", cidr), - } - } - } - - if len(allowOut) > 0 { - _, allowedDomains := sandbox_network.ParseAddressesAndDomains(allowOut) - hasBlockAll := slices.Contains(denyOut, sandbox_network.AllInternetTrafficCIDR) - - if len(allowedDomains) > 0 && !hasBlockAll { - return &api.APIError{ - Code: http.StatusBadRequest, - Err: fmt.Errorf("allow out contains domains but deny out is missing 0.0.0.0/0 (ALL_TRAFFIC)"), - ClientMsg: ErrMsgDomainsRequireBlockAll, - } - } + if apiErr := validateEgressRules(allowOut, denyOut); apiErr != nil { + return apiErr } - return nil + return validateIngressRules(&types.SandboxNetworkIngressConfig{ + AllowedPorts: intsToUint32s(network.AllowPorts), + DeniedPorts: intsToUint32s(network.DenyPorts), + AllowedClientCIDRs: sharedUtils.DerefOrDefault(network.AllowIn, nil), + DeniedClientCIDRs: sharedUtils.DerefOrDefault(network.DenyIn, nil), + }) } diff --git a/packages/api/internal/handlers/sandbox_create_test.go b/packages/api/internal/handlers/sandbox_create_test.go index 59b3dc57df..d752f5bfd5 100644 --- a/packages/api/internal/handlers/sandbox_create_test.go +++ b/packages/api/internal/handlers/sandbox_create_test.go @@ -240,6 +240,109 @@ func TestValidateNetworkConfig(t *testing.T) { }, wantErr: false, }, + // Ingress port validation tests + { + name: "valid allowPorts", + network: &api.SandboxNetworkConfig{ + AllowPorts: &[]int{80, 443, 8080}, + }, + wantErr: false, + }, + { + name: "allowPorts with port 0 is invalid", + network: &api.SandboxNetworkConfig{ + AllowPorts: &[]int{0}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "invalid allowPorts port 0: must be between 1 and 65535", + }, + { + name: "allowPorts with port > 65535 is invalid", + network: &api.SandboxNetworkConfig{ + AllowPorts: &[]int{70000}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "invalid allowPorts port 70000: must be between 1 and 65535", + }, + { + name: "valid denyPorts", + network: &api.SandboxNetworkConfig{ + DenyPorts: &[]int{22, 3306}, + }, + wantErr: false, + }, + { + name: "denyPorts with port 0 is invalid", + network: &api.SandboxNetworkConfig{ + DenyPorts: &[]int{0}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "invalid denyPorts port 0: must be between 1 and 65535", + }, + // Ingress CIDR validation tests + { + name: "valid allowIn CIDR with deny-all", + network: &api.SandboxNetworkConfig{ + AllowIn: &[]string{"10.0.0.0/8"}, + DenyIn: &[]string{"0.0.0.0/0"}, + }, + wantErr: false, + }, + { + name: "valid allowIn bare IP with deny-all", + network: &api.SandboxNetworkConfig{ + AllowIn: &[]string{"1.2.3.4"}, + DenyIn: &[]string{"0.0.0.0/0"}, + }, + wantErr: false, + }, + { + name: "allowIn without deny-all is rejected", + network: &api.SandboxNetworkConfig{ + AllowIn: &[]string{"10.0.0.0/8"}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "When specifying allowed CIDRs in allowIn, you must include '0.0.0.0/0' in denyIn to block all other traffic.", + }, + { + name: "allowIn with partial denyIn is rejected", + network: &api.SandboxNetworkConfig{ + AllowIn: &[]string{"10.0.0.0/8"}, + DenyIn: &[]string{"192.168.0.0/16"}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "When specifying allowed CIDRs in allowIn, you must include '0.0.0.0/0' in denyIn to block all other traffic.", + }, + { + name: "invalid allowIn entry", + network: &api.SandboxNetworkConfig{ + AllowIn: &[]string{"not-a-cidr"}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "invalid allowIn CIDR not-a-cidr", + }, + { + name: "valid denyIn CIDR", + network: &api.SandboxNetworkConfig{ + DenyIn: &[]string{"192.168.0.0/16"}, + }, + wantErr: false, + }, + { + name: "invalid denyIn entry", + network: &api.SandboxNetworkConfig{ + DenyIn: &[]string{"bad"}, + }, + wantErr: true, + wantCode: http.StatusBadRequest, + wantErrMsg: "invalid denyIn CIDR bad", + }, // Mixed domain and CIDR tests { name: "allow_out with domain and CIDR without deny_out block-all is invalid", diff --git a/packages/api/internal/handlers/sandbox_network_update.go b/packages/api/internal/handlers/sandbox_network_update.go index f682a3ff2b..e803bbf824 100644 --- a/packages/api/internal/handlers/sandbox_network_update.go +++ b/packages/api/internal/handlers/sandbox_network_update.go @@ -3,13 +3,19 @@ package handlers import ( "fmt" "net/http" + "slices" + "strings" "github.com/gin-gonic/gin" + "golang.org/x/net/idna" "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/db/pkg/types" + sandbox_network "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network" "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" + sutils "github.com/e2b-dev/infra/packages/shared/pkg/utils" ) func (a *APIStore) PutSandboxesSandboxIDNetwork( @@ -36,23 +42,32 @@ func (a *APIStore) PutSandboxesSandboxIDNetwork( return } - var allowedEntries []string - if body.AllowOut != nil { - allowedEntries = *body.AllowOut + egressUpdate := &types.SandboxNetworkEgressConfig{ + AllowedAddresses: sutils.DerefOrDefault(body.AllowOut, nil), + DeniedAddresses: sutils.DerefOrDefault(body.DenyOut, nil), } - var deniedEntries []string - if body.DenyOut != nil { - deniedEntries = *body.DenyOut + ingressUpdate := &types.SandboxNetworkIngressConfig{ + MaskRequestHost: body.MaskRequestHost, + AllowedPorts: intsToUint32s(body.AllowPorts), + DeniedPorts: intsToUint32s(body.DenyPorts), + AllowedClientCIDRs: sutils.DerefOrDefault(body.AllowIn, nil), + DeniedClientCIDRs: sutils.DerefOrDefault(body.DenyIn, nil), } - if apiErr := validateEgressRules(allowedEntries, deniedEntries); apiErr != nil { + if apiErr := validateEgressRules(egressUpdate.AllowedAddresses, egressUpdate.DeniedAddresses); apiErr != nil { a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) return } - if apiErr := a.orchestrator.UpdateSandboxNetworkConfig(ctx, team.ID, sandboxID, allowedEntries, deniedEntries); apiErr != nil { + if apiErr := validateIngressRules(ingressUpdate); apiErr != nil { + a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) + + return + } + + if apiErr := a.orchestrator.UpdateSandboxNetworkConfig(ctx, team.ID, sandboxID, egressUpdate, ingressUpdate); apiErr != nil { telemetry.ReportErrorByCode(ctx, apiErr.Code, "error updating sandbox network config", apiErr.Err) a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) @@ -61,3 +76,125 @@ func (a *APIStore) PutSandboxesSandboxIDNetwork( c.Status(http.StatusNoContent) } + +func intsToUint32s(ints *[]int) []uint32 { + if ints == nil { + return nil + } + + result := make([]uint32, len(*ints)) + for i, v := range *ints { + result[i] = uint32(v) + } + + return result +} + +// validateEgressRules validates egress allow/deny rules: +// - denyOut entries must be valid IPs or CIDRs (not domains) +// - allowOut entries must be valid IPs, CIDRs, or domain names +// - when allowOut contains domains, denyOut must include 0.0.0.0/0 +func validateEgressRules(allowOut, denyOut []string) *api.APIError { + for _, cidr := range denyOut { + if !sandbox_network.IsIPOrCIDR(cidr) { + return &api.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("invalid denied CIDR %s", cidr), + ClientMsg: fmt.Sprintf("invalid denied CIDR %s", cidr), + } + } + } + + if len(allowOut) > 0 { + _, allowedDomains := sandbox_network.ParseAddressesAndDomains(allowOut) + + for _, domain := range allowedDomains { + // Strip wildcard prefix for IDNA validation (*.example.com -> example.com). + // The "*" label is not a valid IDNA label, but we support it as a wildcard. + validateDomain := domain + if strings.HasPrefix(domain, "*.") { + validateDomain = domain[2:] + } + + if validateDomain != "*" { + if _, err := idna.Lookup.ToASCII(validateDomain); err != nil { + return &api.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("invalid allowed domain %q: %w", domain, err), + ClientMsg: fmt.Sprintf("invalid allowed domain: %s", domain), + } + } + } + } + + hasBlockAll := slices.Contains(denyOut, sandbox_network.AllInternetTrafficCIDR) + + if len(allowedDomains) > 0 && !hasBlockAll { + return &api.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("allow out contains domains but deny out is missing 0.0.0.0/0 (ALL_TRAFFIC)"), + ClientMsg: ErrMsgDomainsRequireBlockAll, + } + } + } + + return nil +} + +func validateIngressRules(ingress *types.SandboxNetworkIngressConfig) *api.APIError { + if apiErr := validatePortList(ingress.AllowedPorts, "allowPorts"); apiErr != nil { + return apiErr + } + + if apiErr := validatePortList(ingress.DeniedPorts, "denyPorts"); apiErr != nil { + return apiErr + } + + if apiErr := validateCIDRList(ingress.AllowedClientCIDRs, "allowIn"); apiErr != nil { + return apiErr + } + + if apiErr := validateCIDRList(ingress.DeniedClientCIDRs, "denyIn"); apiErr != nil { + return apiErr + } + + // Consistent with egress: allowIn without deny-all is a no-op (default is allow-all), + // so require 0.0.0.0/0 in denyIn to prevent a silent misconfiguration. + if len(ingress.AllowedClientCIDRs) > 0 && !slices.Contains(ingress.DeniedClientCIDRs, sandbox_network.AllInternetTrafficCIDR) { + return &api.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("allowIn is set but denyIn is missing 0.0.0.0/0 (ALL_TRAFFIC)"), + ClientMsg: "When specifying allowed CIDRs in allowIn, you must include '0.0.0.0/0' in denyIn to block all other traffic.", + } + } + + return nil +} + +func validatePortList(ports []uint32, fieldName string) *api.APIError { + for _, p := range ports { + if p == 0 || p > 65535 { + return &api.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("invalid %s port %d", fieldName, p), + ClientMsg: fmt.Sprintf("invalid %s port %d: must be between 1 and 65535", fieldName, p), + } + } + } + + return nil +} + +func validateCIDRList(cidrs []string, fieldName string) *api.APIError { + for _, cidr := range cidrs { + if !sandbox_network.IsIPOrCIDR(cidr) { + return &api.APIError{ + Code: http.StatusBadRequest, + Err: fmt.Errorf("invalid %s CIDR %s", fieldName, cidr), + ClientMsg: fmt.Sprintf("invalid %s CIDR %s", fieldName, cidr), + } + } + } + + return nil +} diff --git a/packages/api/internal/orchestrator/create_instance.go b/packages/api/internal/orchestrator/create_instance.go index 219050be47..1561c9ef07 100644 --- a/packages/api/internal/orchestrator/create_instance.go +++ b/packages/api/internal/orchestrator/create_instance.go @@ -64,6 +64,10 @@ func buildNetworkConfig(network *types.SandboxNetworkConfig, allowInternetAccess if network != nil && network.Ingress != nil { orchNetwork.Ingress.MaskRequestHost = network.Ingress.MaskRequestHost + orchNetwork.Ingress.AllowedPorts = network.Ingress.AllowedPorts + orchNetwork.Ingress.DeniedPorts = network.Ingress.DeniedPorts + orchNetwork.Ingress.AllowedClientCidrs = network.Ingress.AllowedClientCIDRs + orchNetwork.Ingress.DeniedClientCidrs = network.Ingress.DeniedClientCIDRs } // Handle the case where internet access is explicitly disabled diff --git a/packages/api/internal/orchestrator/update_network.go b/packages/api/internal/orchestrator/update_network.go index 1708a4902b..957a1b03c0 100644 --- a/packages/api/internal/orchestrator/update_network.go +++ b/packages/api/internal/orchestrator/update_network.go @@ -24,10 +24,18 @@ func (o *Orchestrator) UpdateSandboxNetworkConfig( ctx context.Context, teamID uuid.UUID, sandboxID string, - allowedEntries []string, - deniedEntries []string, + egressUpdate *types.SandboxNetworkEgressConfig, + ingressUpdate *types.SandboxNetworkIngressConfig, ) *api.APIError { - egress := buildEgressConfig(allowedEntries, deniedEntries) + egressProto := buildEgressConfig(egressUpdate.AllowedAddresses, egressUpdate.DeniedAddresses) + + ingressProto := &orchestratorgrpc.SandboxNetworkIngressConfig{ + MaskRequestHost: ingressUpdate.MaskRequestHost, + AllowedPorts: ingressUpdate.AllowedPorts, + DeniedPorts: ingressUpdate.DeniedPorts, + AllowedClientCidrs: ingressUpdate.AllowedClientCIDRs, + DeniedClientCidrs: ingressUpdate.DeniedClientCIDRs, + } updateFunc := func(sbx sandbox.Sandbox) (sandbox.Sandbox, error) { if sbx.State != sandbox.StateRunning { @@ -38,10 +46,10 @@ func (o *Orchestrator) UpdateSandboxNetworkConfig( sbx.Network = &types.SandboxNetworkConfig{} } - sbx.Network.Egress = &types.SandboxNetworkEgressConfig{ - AllowedAddresses: allowedEntries, - DeniedAddresses: deniedEntries, - } + sbx.Network.Egress = egressUpdate + sbx.Network.Ingress = ingressUpdate + + ingressProto.TrafficAccessToken = sbx.TrafficAccessToken return sbx, nil } @@ -62,13 +70,14 @@ func (o *Orchestrator) UpdateSandboxNetworkConfig( } // Apply the network update on the orchestrator node. - return o.updateSandboxNetworkOnNode(ctx, sbx, egress) + return o.updateSandboxNetworkOnNode(ctx, sbx, egressProto, ingressProto) } func (o *Orchestrator) updateSandboxNetworkOnNode( ctx context.Context, sbx sandbox.Sandbox, egress *orchestratorgrpc.SandboxNetworkEgressConfig, + ingress *orchestratorgrpc.SandboxNetworkIngressConfig, ) *api.APIError { ctx, span := tracer.Start(ctx, "update-sandbox-network-on-node", trace.WithAttributes( @@ -90,6 +99,7 @@ func (o *Orchestrator) updateSandboxNetworkOnNode( _, err := client.Sandbox.Update(ctx, &orchestratorgrpc.SandboxUpdateRequest{ SandboxId: sbx.SandboxID, Egress: egress, + Ingress: ingress, }) if err != nil { grpcErr, ok := status.FromError(err) diff --git a/packages/client-proxy/internal/proxy/proxy.go b/packages/client-proxy/internal/proxy/proxy.go index 80d38df291..197f5bd403 100644 --- a/packages/client-proxy/internal/proxy/proxy.go +++ b/packages/client-proxy/internal/proxy/proxy.go @@ -167,6 +167,12 @@ func NewClientProxy(meterProvider metric.MeterProvider, serviceName string, port zap.String("target_port", url.Port()), ) + // Forward the real client IP to the orchestrator via an internal header. + // Delete any client-supplied value first so ExtractClientIP derives + // the IP from trusted sources (XFF / RemoteAddr) only. + r.Header.Del(reverseproxy.ClientIPHeader) + r.Header.Set(reverseproxy.ClientIPHeader, reverseproxy.ExtractClientIP(r)) + return &pool.Destination{ SandboxId: sandboxId, RequestLogger: l, diff --git a/packages/db/pkg/types/types.go b/packages/db/pkg/types/types.go index e9cede9553..298aa6717d 100644 --- a/packages/db/pkg/types/types.go +++ b/packages/db/pkg/types/types.go @@ -20,8 +20,12 @@ type SandboxNetworkEgressConfig struct { const AllowPublicAccessDefault = true type SandboxNetworkIngressConfig struct { - AllowPublicAccess *bool `json:"allowPublicAccess,omitempty"` - MaskRequestHost *string `json:"maskRequestHost,omitempty"` + AllowPublicAccess *bool `json:"allowPublicAccess,omitempty"` + MaskRequestHost *string `json:"maskRequestHost,omitempty"` + AllowedPorts []uint32 `json:"allowedPorts,omitempty"` + DeniedPorts []uint32 `json:"deniedPorts,omitempty"` + AllowedClientCIDRs []string `json:"allowedClientCidrs,omitempty"` + DeniedClientCIDRs []string `json:"deniedClientCidrs,omitempty"` } type SandboxNetworkConfig struct { diff --git a/packages/orchestrator/internal/proxy/proxy.go b/packages/orchestrator/internal/proxy/proxy.go index 0edba17015..1f066bc5e0 100644 --- a/packages/orchestrator/internal/proxy/proxy.go +++ b/packages/orchestrator/internal/proxy/proxy.go @@ -3,6 +3,7 @@ package proxy import ( "context" "fmt" + "net" "net/http" "net/url" "strconv" @@ -88,6 +89,32 @@ func NewSandboxProxy(meterProvider metric.MeterProvider, port uint16, sandboxes } } + if isNonEnvdTraffic { + // Handle port allowlist/denylist for non-envd traffic. + // Priority: allow wins → deny → default allow. + if !containsPort(ingress.GetAllowedPorts(), port) { + if containsPort(ingress.GetDeniedPorts(), port) { + return nil, reverseproxy.NewErrPortNotAllowed(sandboxId, port) + } + } + + // Handle client IP allowlist/denylist for non-envd traffic. + // Priority: allow wins → deny → default allow. + // Uses pre-parsed CIDRs for performance. + if sbx.HasIngressClientCIDRs() { + clientIP := reverseproxy.ExtractClientIP(r) + if ip := net.ParseIP(clientIP); ip != nil { + if !sbx.IngressAllowsClientIP(ip) && sbx.IngressDeniesClientIP(ip) { + return nil, reverseproxy.NewErrClientIPNotAllowed(sandboxId, clientIP) + } + } + } + } + + // Strip the internal client IP header so it never reaches the sandbox. + // Must happen after extractClientIP reads it, and before Rewrite clones r into r.Out. + r.Header.Del(reverseproxy.ClientIPHeader) + // Handle request host masking only for non-envd traffic. var maskRequestHost *string = nil if h := ingress.GetMaskRequestHost(); isNonEnvdTraffic && h != "" { @@ -206,3 +233,14 @@ func (p *SandboxProxy) OnInsert(_ *sandbox.Sandbox) {} func (p *SandboxProxy) OnRemove(sandboxID string) { p.limiter.Remove(sandboxID) } + +// containsPort checks if a port is in the given list of ports. +func containsPort(ports []uint32, port uint64) bool { + for _, p := range ports { + if uint64(p) == port { + return true + } + } + + return false +} diff --git a/packages/orchestrator/internal/proxy/proxy_test.go b/packages/orchestrator/internal/proxy/proxy_test.go new file mode 100644 index 0000000000..9f4022c479 --- /dev/null +++ b/packages/orchestrator/internal/proxy/proxy_test.go @@ -0,0 +1,75 @@ +package proxy + +import ( + "net/http" + "testing" + + reverseproxy "github.com/e2b-dev/infra/packages/shared/pkg/proxy" +) + +func TestExtractClientIP(t *testing.T) { + t.Parallel() + tests := []struct { + name string + clientIP string // X-E2B-Client-IP header + xff string + remoteAddr string + want string + }{ + { + name: "X-E2B-Client-IP takes priority over everything", + clientIP: "203.0.113.42", + xff: "1.2.3.4", + remoteAddr: "5.6.7.8:1234", + want: "203.0.113.42", + }, + { + name: "X-Forwarded-For single IP", + xff: "1.2.3.4", + remoteAddr: "5.6.7.8:1234", + want: "1.2.3.4", + }, + { + name: "X-Forwarded-For multiple IPs takes second-to-last", + xff: "1.2.3.4, 10.0.0.1, 172.16.0.1", + remoteAddr: "5.6.7.8:1234", + want: "10.0.0.1", + }, + { + name: "X-Forwarded-For two IPs takes first (second-to-last)", + xff: " 1.2.3.4 , 10.0.0.1", + remoteAddr: "5.6.7.8:1234", + want: "1.2.3.4", + }, + { + name: "no headers falls back to RemoteAddr", + remoteAddr: "5.6.7.8:1234", + want: "5.6.7.8", + }, + { + name: "RemoteAddr without port", + remoteAddr: "5.6.7.8", + want: "5.6.7.8", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &http.Request{ + Header: http.Header{}, + RemoteAddr: tt.remoteAddr, + } + if tt.clientIP != "" { + r.Header.Set(reverseproxy.ClientIPHeader, tt.clientIP) + } + if tt.xff != "" { + r.Header.Set("X-Forwarded-For", tt.xff) + } + + if got := reverseproxy.ExtractClientIP(r); got != tt.want { + t.Errorf("ExtractClientIP() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/packages/orchestrator/internal/sandbox/metadata_ingress_test.go b/packages/orchestrator/internal/sandbox/metadata_ingress_test.go new file mode 100644 index 0000000000..d046722a98 --- /dev/null +++ b/packages/orchestrator/internal/sandbox/metadata_ingress_test.go @@ -0,0 +1,115 @@ +package sandbox + +import ( + "net" + "testing" + + "github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator" +) + +func TestIngressClientCIDRs_AllowDenyPriority(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + allowed []string + denied []string + ip string + wantAllowed bool + wantDenied bool + }{ + { + name: "IP in allowed CIDR", + allowed: []string{"10.0.0.0/8"}, + ip: "10.1.2.3", + wantAllowed: true, + wantDenied: false, + }, + { + name: "IP in denied CIDR", + denied: []string{"10.0.0.0/8"}, + ip: "10.1.2.3", + wantDenied: true, + }, + { + name: "IP in both allowed and denied — both report true", + allowed: []string{"10.0.0.0/8"}, + denied: []string{"10.1.0.0/16"}, + ip: "10.1.2.3", + wantAllowed: true, + wantDenied: true, + }, + { + name: "IP in neither", + allowed: []string{"192.168.0.0/16"}, + denied: []string{"172.16.0.0/12"}, + ip: "10.1.2.3", + }, + { + name: "bare IP allowed", + allowed: []string{"1.2.3.4"}, + ip: "1.2.3.4", + wantAllowed: true, + }, + { + name: "bare IP denied", + denied: []string{"1.2.3.4"}, + ip: "1.2.3.4", + wantDenied: true, + }, + { + name: "bare IP no match", + allowed: []string{"1.2.3.4"}, + ip: "1.2.3.5", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + m := &Metadata{} + m.SetNetworkIngress(&orchestrator.SandboxNetworkIngressConfig{ + AllowedClientCidrs: tt.allowed, + DeniedClientCidrs: tt.denied, + }) + + ip := net.ParseIP(tt.ip) + if ip == nil { + t.Fatalf("failed to parse IP: %s", tt.ip) + } + + if got := m.IngressAllowsClientIP(ip); got != tt.wantAllowed { + t.Errorf("IngressAllowsClientIP(%s) = %v, want %v", tt.ip, got, tt.wantAllowed) + } + if got := m.IngressDeniesClientIP(ip); got != tt.wantDenied { + t.Errorf("IngressDeniesClientIP(%s) = %v, want %v", tt.ip, got, tt.wantDenied) + } + }) + } +} + +func TestIngressClientCIDRs_SetNetworkIngress_Replaces(t *testing.T) { + t.Parallel() + + m := &Metadata{} + m.SetNetworkIngress(&orchestrator.SandboxNetworkIngressConfig{ + AllowedClientCidrs: []string{"10.0.0.0/8"}, + }) + + if !m.IngressAllowsClientIP(net.ParseIP("10.1.2.3")) { + t.Fatal("expected 10.1.2.3 to be allowed initially") + } + + // Replace with different config. + m.SetNetworkIngress(&orchestrator.SandboxNetworkIngressConfig{ + DeniedClientCidrs: []string{"10.0.0.0/8"}, + }) + + if m.IngressAllowsClientIP(net.ParseIP("10.1.2.3")) { + t.Error("expected 10.1.2.3 to NOT be allowed after replacement") + } + if !m.IngressDeniesClientIP(net.ParseIP("10.1.2.3")) { + t.Error("expected 10.1.2.3 to be denied after replacement") + } +} diff --git a/packages/orchestrator/internal/sandbox/sandbox.go b/packages/orchestrator/internal/sandbox/sandbox.go index 94ba58508e..d2f9abe4d8 100644 --- a/packages/orchestrator/internal/sandbox/sandbox.go +++ b/packages/orchestrator/internal/sandbox/sandbox.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net" "net/http" "sync" "sync/atomic" @@ -112,6 +113,14 @@ func (c *Config) SetNetworkEgress(egress *orchestrator.SandboxNetworkEgressConfi c.Network.Egress = egress } +// SetNetworkIngress updates the sandbox network ingress config in a thread-safe manner. +func (c *Config) SetNetworkIngress(ingress *orchestrator.SandboxNetworkIngressConfig) { + c.mu.Lock() + defer c.mu.Unlock() + + c.Network.Ingress = ingress +} + // GetNetworkIngress returns the ingress config in a thread-safe manner. func (c *Config) GetNetworkIngress() *orchestrator.SandboxNetworkIngressConfig { c.mu.RLock() @@ -180,9 +189,15 @@ type Metadata struct { Config *Config Runtime RuntimeMetadata - rwmu sync.RWMutex // protects startedAt, endAt + rwmu sync.RWMutex // protects startedAt, endAt, ingress CIDRs startedAt time.Time endAt time.Time + + // Pre-parsed ingress CIDRs for fast matching on the proxy hot path. + allowedClientNets []*net.IPNet + allowedClientIPs []net.IP + deniedClientNets []*net.IPNet + deniedClientIPs []net.IP } // GetEndAt returns the sandbox end time in a thread-safe manner. @@ -201,6 +216,109 @@ func (m *Metadata) SetEndAt(t time.Time) { m.endAt = t } +// SetNetworkIngress updates the sandbox network ingress config in a thread-safe manner. +// It updates the Config and also pre-parses CIDRs for fast matching on the proxy hot path. +func (m *Metadata) SetNetworkIngress(ingress *orchestrator.SandboxNetworkIngressConfig) { + if m.Config != nil { + m.Config.SetNetworkIngress(ingress) + } + + m.rwmu.Lock() + defer m.rwmu.Unlock() + + m.rebuildIngressCIDRs(ingress) +} + +// rebuildIngressCIDRs parses CIDR strings from the given ingress config into net.IPNet/net.IP. +// Must be called with rwmu held. +func (m *Metadata) rebuildIngressCIDRs(ingress *orchestrator.SandboxNetworkIngressConfig) { + m.allowedClientNets = nil + m.allowedClientIPs = nil + m.deniedClientNets = nil + m.deniedClientIPs = nil + + if ingress == nil { + return + } + + m.allowedClientNets, m.allowedClientIPs = parseCIDRList(ingress.GetAllowedClientCidrs()) + m.deniedClientNets, m.deniedClientIPs = parseCIDRList(ingress.GetDeniedClientCidrs()) +} + +// buildParsedIngressCIDRs reads the current ingress config from Config and parses CIDRs. +// Must be called with rwmu held. +func (m *Metadata) buildParsedIngressCIDRs() { + if m.Config == nil { + m.rebuildIngressCIDRs(nil) + + return + } + + m.rebuildIngressCIDRs(m.Config.GetNetworkIngress()) +} + +// IngressAllowsClientIP returns true if the IP matches any allowed CIDR/IP. +func (m *Metadata) IngressAllowsClientIP(ip net.IP) bool { + m.rwmu.RLock() + defer m.rwmu.RUnlock() + + return ipMatchesList(ip, m.allowedClientNets, m.allowedClientIPs) +} + +// IngressDeniesClientIP returns true if the IP matches any denied CIDR/IP. +func (m *Metadata) IngressDeniesClientIP(ip net.IP) bool { + m.rwmu.RLock() + defer m.rwmu.RUnlock() + + return ipMatchesList(ip, m.deniedClientNets, m.deniedClientIPs) +} + +// HasIngressClientCIDRs returns true if any client CIDR rules are configured. +func (m *Metadata) HasIngressClientCIDRs() bool { + m.rwmu.RLock() + defer m.rwmu.RUnlock() + + return len(m.allowedClientNets) > 0 || len(m.allowedClientIPs) > 0 || + len(m.deniedClientNets) > 0 || len(m.deniedClientIPs) > 0 +} + +func ipMatchesList(ip net.IP, nets []*net.IPNet, ips []net.IP) bool { + for _, n := range nets { + if n.Contains(ip) { + return true + } + } + + for _, addr := range ips { + if addr.Equal(ip) { + return true + } + } + + return false +} + +// parseCIDRList parses a list of CIDR/IP strings into nets and bare IPs. +func parseCIDRList(cidrs []string) ([]*net.IPNet, []net.IP) { + var nets []*net.IPNet + var ips []net.IP + + for _, cidr := range cidrs { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + if parsed := net.ParseIP(cidr); parsed != nil { + ips = append(ips, parsed) + } + + continue + } + + nets = append(nets, ipNet) + } + + return nets, ips +} + type Sandbox struct { *Resources *Metadata @@ -427,6 +545,7 @@ func (f *Factory) CreateSandbox( startedAt: time.Now(), endAt: time.Now().Add(sandboxTimeout), } + metadata.buildParsedIngressCIDRs() sbx := &Sandbox{ LifecycleID: lifecycleID, @@ -750,6 +869,7 @@ func (f *Factory) ResumeSandbox( startedAt: startedAt, endAt: endAt, } + metadata.buildParsedIngressCIDRs() sbx := &Sandbox{ LifecycleID: lifecycleID, diff --git a/packages/orchestrator/internal/server/sandboxes.go b/packages/orchestrator/internal/server/sandboxes.go index 231a7ef69d..ff872fc918 100644 --- a/packages/orchestrator/internal/server/sandboxes.go +++ b/packages/orchestrator/internal/server/sandboxes.go @@ -268,12 +268,14 @@ func (s *Server) Update(ctx context.Context, req *orchestrator.SandboxUpdateRequ return nil, status.Error(codes.NotFound, "sandbox not found") } + teamID, buildId, eventData := s.prepareSandboxEventData(ctx, sbx) var updates []utils.UpdateFunc if req.GetEndTime() != nil { updates = append(updates, func(_ context.Context) (func(context.Context), error) { old := sbx.GetEndAt() sbx.SetEndAt(req.GetEndTime().AsTime()) + eventData["set_timeout"] = req.GetEndTime().AsTime().Format(time.RFC3339) return func(_ context.Context) { sbx.SetEndAt(old) }, nil }) @@ -294,6 +296,12 @@ func (s *Server) Update(ctx context.Context, req *orchestrator.SandboxUpdateRequ sbx.Config.SetNetworkEgress(egress) } + eventData["network_egress"] = map[string]any{ + "allowed_cidrs": egress.GetAllowedCidrs(), + "denied_cidrs": egress.GetDeniedCidrs(), + "allowed_domains": egress.GetAllowedDomains(), + } + return func(ctx context.Context) { _ = sbx.Slot.UpdateInternet(ctx, oldEgress) sbx.Config.SetNetworkEgress(oldEgress) @@ -301,6 +309,23 @@ func (s *Server) Update(ctx context.Context, req *orchestrator.SandboxUpdateRequ }) } + if req.GetIngress() != nil { + updates = append(updates, func(_ context.Context) (func(context.Context), error) { + oldIngress := sbx.Config.GetNetworkIngress() + sbx.SetNetworkIngress(req.GetIngress()) + + ingress := req.GetIngress() + eventData["network_ingress"] = map[string]any{ + "allowed_ports": ingress.GetAllowedPorts(), + "denied_ports": ingress.GetDeniedPorts(), + "allowed_client_cidrs": ingress.GetAllowedClientCidrs(), + "denied_client_cidrs": ingress.GetDeniedClientCidrs(), + } + + return func(_ context.Context) { sbx.SetNetworkIngress(oldIngress) }, nil + }) + } + if err := utils.ApplyAllOrNone(ctx, updates); err != nil { telemetry.ReportCriticalError(ctx, "failed to update sandbox", err) @@ -309,18 +334,6 @@ func (s *Server) Update(ctx context.Context, req *orchestrator.SandboxUpdateRequ // Publish event if any updates were applied. if len(updates) > 0 { - teamID, buildId, eventData := s.prepareSandboxEventData(ctx, sbx) - if req.GetEndTime() != nil { - eventData["set_timeout"] = req.GetEndTime().AsTime().Format(time.RFC3339) - } - if egress := req.GetEgress(); egress != nil { - eventData["network_egress"] = map[string]any{ - "allowed_cidrs": egress.GetAllowedCidrs(), - "denied_cidrs": egress.GetDeniedCidrs(), - "allowed_domains": egress.GetAllowedDomains(), - } - } - go s.sbxEventsService.Publish( context.WithoutCancel(ctx), teamID, diff --git a/packages/orchestrator/orchestrator.proto b/packages/orchestrator/orchestrator.proto index de8d7378ce..9f3b941d8e 100644 --- a/packages/orchestrator/orchestrator.proto +++ b/packages/orchestrator/orchestrator.proto @@ -80,6 +80,10 @@ message SandboxNetworkEgressConfig { message SandboxNetworkIngressConfig { optional string traffic_access_token = 1; optional string mask_request_host = 2; + repeated uint32 allowed_ports = 3; + repeated uint32 denied_ports = 4; + repeated string allowed_client_cidrs = 5; + repeated string denied_client_cidrs = 6; } message SandboxCreateRequest { @@ -99,6 +103,7 @@ message SandboxUpdateRequest { // All fields are optional — only set fields are applied. optional google.protobuf.Timestamp end_time = 2; optional SandboxNetworkEgressConfig egress = 3; + optional SandboxNetworkIngressConfig ingress = 4; } message SandboxDeleteRequest { diff --git a/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go b/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go index 3631fa392b..cda046a54a 100644 --- a/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go +++ b/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go @@ -502,8 +502,12 @@ type SandboxNetworkIngressConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - TrafficAccessToken *string `protobuf:"bytes,1,opt,name=traffic_access_token,json=trafficAccessToken,proto3,oneof" json:"traffic_access_token,omitempty"` - MaskRequestHost *string `protobuf:"bytes,2,opt,name=mask_request_host,json=maskRequestHost,proto3,oneof" json:"mask_request_host,omitempty"` + TrafficAccessToken *string `protobuf:"bytes,1,opt,name=traffic_access_token,json=trafficAccessToken,proto3,oneof" json:"traffic_access_token,omitempty"` + MaskRequestHost *string `protobuf:"bytes,2,opt,name=mask_request_host,json=maskRequestHost,proto3,oneof" json:"mask_request_host,omitempty"` + AllowedPorts []uint32 `protobuf:"varint,3,rep,packed,name=allowed_ports,json=allowedPorts,proto3" json:"allowed_ports,omitempty"` + DeniedPorts []uint32 `protobuf:"varint,4,rep,packed,name=denied_ports,json=deniedPorts,proto3" json:"denied_ports,omitempty"` + AllowedClientCidrs []string `protobuf:"bytes,5,rep,name=allowed_client_cidrs,json=allowedClientCidrs,proto3" json:"allowed_client_cidrs,omitempty"` + DeniedClientCidrs []string `protobuf:"bytes,6,rep,name=denied_client_cidrs,json=deniedClientCidrs,proto3" json:"denied_client_cidrs,omitempty"` } func (x *SandboxNetworkIngressConfig) Reset() { @@ -552,6 +556,34 @@ func (x *SandboxNetworkIngressConfig) GetMaskRequestHost() string { return "" } +func (x *SandboxNetworkIngressConfig) GetAllowedPorts() []uint32 { + if x != nil { + return x.AllowedPorts + } + return nil +} + +func (x *SandboxNetworkIngressConfig) GetDeniedPorts() []uint32 { + if x != nil { + return x.DeniedPorts + } + return nil +} + +func (x *SandboxNetworkIngressConfig) GetAllowedClientCidrs() []string { + if x != nil { + return x.AllowedClientCidrs + } + return nil +} + +func (x *SandboxNetworkIngressConfig) GetDeniedClientCidrs() []string { + if x != nil { + return x.DeniedClientCidrs + } + return nil +} + type SandboxCreateRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -669,8 +701,9 @@ type SandboxUpdateRequest struct { SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` // All fields are optional — only set fields are applied. - EndTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=end_time,json=endTime,proto3,oneof" json:"end_time,omitempty"` - Egress *SandboxNetworkEgressConfig `protobuf:"bytes,3,opt,name=egress,proto3,oneof" json:"egress,omitempty"` + EndTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=end_time,json=endTime,proto3,oneof" json:"end_time,omitempty"` + Egress *SandboxNetworkEgressConfig `protobuf:"bytes,3,opt,name=egress,proto3,oneof" json:"egress,omitempty"` + Ingress *SandboxNetworkIngressConfig `protobuf:"bytes,4,opt,name=ingress,proto3,oneof" json:"ingress,omitempty"` } func (x *SandboxUpdateRequest) Reset() { @@ -726,6 +759,13 @@ func (x *SandboxUpdateRequest) GetEgress() *SandboxNetworkEgressConfig { return nil } +func (x *SandboxUpdateRequest) GetIngress() *SandboxNetworkIngressConfig { + if x != nil { + return x.Ingress + } + return nil +} + type SandboxDeleteRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1260,7 +1300,7 @@ var file_orchestrator_proto_rawDesc = []byte{ 0x69, 0x65, 0x64, 0x43, 0x69, 0x64, 0x72, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x1b, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4e, 0x65, 0x74, + 0x73, 0x22, 0xde, 0x02, 0x0a, 0x1b, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x14, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, @@ -1268,115 +1308,130 @@ var file_orchestrator_proto_rawDesc = []byte{ 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x11, 0x6d, 0x61, 0x73, 0x6b, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0f, 0x6d, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x88, 0x01, 0x01, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x74, 0x72, - 0x61, 0x66, 0x66, 0x69, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x5f, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x22, 0xb2, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x28, 0x0a, 0x07, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x07, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x12, 0x39, 0x0a, 0x0a, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x34, 0x0a, - 0x15, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x49, 0x64, 0x22, 0xc3, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x08, 0x65, - 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x64, - 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x06, 0x65, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x45, 0x67, 0x72, 0x65, 0x73, 0x73, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x06, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, - 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x42, 0x09, - 0x0a, 0x07, 0x5f, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x22, 0x35, 0x0a, 0x14, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, + 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x21, + 0x0a, 0x0c, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0b, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, + 0x73, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x69, + 0x64, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x5f, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x11, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x69, + 0x64, 0x72, 0x73, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x5f, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x14, 0x0a, 0x12, + 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x68, 0x6f, + 0x73, 0x74, 0x22, 0xb2, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x73, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x73, 0x61, + 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, + 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x34, 0x0a, 0x15, 0x53, 0x61, 0x6e, 0x64, 0x62, + 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x8c, 0x02, + 0x0a, 0x14, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, + 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, + 0x62, 0x6f, 0x78, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, + 0x01, 0x12, 0x38, 0x0a, 0x06, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x45, 0x67, 0x72, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, + 0x52, 0x06, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x07, 0x69, + 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, 0x07, 0x69, 0x6e, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x6e, 0x64, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x22, 0x35, 0x0a, 0x14, + 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, + 0x78, 0x49, 0x64, 0x22, 0x70, 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, + 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, + 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x49, 0x64, 0x22, 0x54, 0x0a, 0x18, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x49, 0x64, - 0x22, 0x70, 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x49, 0x64, 0x22, 0x54, 0x0a, 0x18, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x49, 0x64, 0x12, 0x19, 0x0a, - 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x53, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, - 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, - 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, - 0x44, 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x52, 0x75, 0x6e, 0x6e, - 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x65, 0x73, 0x22, 0x71, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x4b, 0x0a, 0x1f, 0x53, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x06, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x43, 0x61, - 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x73, 0x32, 0xbb, 0x03, 0x0a, 0x0e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x37, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, - 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x4c, 0x69, - 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x50, 0x61, 0x75, - 0x73, 0x65, 0x12, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x43, 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, - 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x53, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, - 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x20, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, - 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x32, 0x62, 0x2d, 0x64, 0x65, - 0x76, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, 0x72, 0x63, 0x68, 0x65, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x0e, 0x52, 0x75, 0x6e, + 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x12, 0x26, 0x0a, 0x06, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x53, 0x61, + 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, + 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, + 0x6d, 0x65, 0x22, 0x44, 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x09, 0x73, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x52, + 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x52, 0x09, 0x73, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x22, 0x71, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x4b, 0x0a, 0x1f, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, + 0x0a, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x32, 0xbb, 0x03, 0x0a, 0x0e, 0x53, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x15, + 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, + 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, + 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x2e, + 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x05, + 0x50, 0x61, 0x75, 0x73, 0x65, 0x12, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, + 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x19, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, + 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, + 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x32, 0x62, + 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, 0x72, 0x63, 0x68, 0x65, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1428,31 +1483,32 @@ var file_orchestrator_proto_depIdxs = []int32{ 19, // 9: SandboxCreateRequest.end_time:type_name -> google.protobuf.Timestamp 19, // 10: SandboxUpdateRequest.end_time:type_name -> google.protobuf.Timestamp 4, // 11: SandboxUpdateRequest.egress:type_name -> SandboxNetworkEgressConfig - 0, // 12: RunningSandbox.config:type_name -> SandboxConfig - 19, // 13: RunningSandbox.start_time:type_name -> google.protobuf.Timestamp - 19, // 14: RunningSandbox.end_time:type_name -> google.protobuf.Timestamp - 13, // 15: SandboxListResponse.sandboxes:type_name -> RunningSandbox - 19, // 16: CachedBuildInfo.expiration_time:type_name -> google.protobuf.Timestamp - 15, // 17: SandboxListCachedBuildsResponse.builds:type_name -> CachedBuildInfo - 6, // 18: SandboxService.Create:input_type -> SandboxCreateRequest - 8, // 19: SandboxService.Update:input_type -> SandboxUpdateRequest - 20, // 20: SandboxService.List:input_type -> google.protobuf.Empty - 9, // 21: SandboxService.Delete:input_type -> SandboxDeleteRequest - 10, // 22: SandboxService.Pause:input_type -> SandboxPauseRequest - 11, // 23: SandboxService.Checkpoint:input_type -> SandboxCheckpointRequest - 20, // 24: SandboxService.ListCachedBuilds:input_type -> google.protobuf.Empty - 7, // 25: SandboxService.Create:output_type -> SandboxCreateResponse - 20, // 26: SandboxService.Update:output_type -> google.protobuf.Empty - 14, // 27: SandboxService.List:output_type -> SandboxListResponse - 20, // 28: SandboxService.Delete:output_type -> google.protobuf.Empty - 20, // 29: SandboxService.Pause:output_type -> google.protobuf.Empty - 12, // 30: SandboxService.Checkpoint:output_type -> SandboxCheckpointResponse - 16, // 31: SandboxService.ListCachedBuilds:output_type -> SandboxListCachedBuildsResponse - 25, // [25:32] is the sub-list for method output_type - 18, // [18:25] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 5, // 12: SandboxUpdateRequest.ingress:type_name -> SandboxNetworkIngressConfig + 0, // 13: RunningSandbox.config:type_name -> SandboxConfig + 19, // 14: RunningSandbox.start_time:type_name -> google.protobuf.Timestamp + 19, // 15: RunningSandbox.end_time:type_name -> google.protobuf.Timestamp + 13, // 16: SandboxListResponse.sandboxes:type_name -> RunningSandbox + 19, // 17: CachedBuildInfo.expiration_time:type_name -> google.protobuf.Timestamp + 15, // 18: SandboxListCachedBuildsResponse.builds:type_name -> CachedBuildInfo + 6, // 19: SandboxService.Create:input_type -> SandboxCreateRequest + 8, // 20: SandboxService.Update:input_type -> SandboxUpdateRequest + 20, // 21: SandboxService.List:input_type -> google.protobuf.Empty + 9, // 22: SandboxService.Delete:input_type -> SandboxDeleteRequest + 10, // 23: SandboxService.Pause:input_type -> SandboxPauseRequest + 11, // 24: SandboxService.Checkpoint:input_type -> SandboxCheckpointRequest + 20, // 25: SandboxService.ListCachedBuilds:input_type -> google.protobuf.Empty + 7, // 26: SandboxService.Create:output_type -> SandboxCreateResponse + 20, // 27: SandboxService.Update:output_type -> google.protobuf.Empty + 14, // 28: SandboxService.List:output_type -> SandboxListResponse + 20, // 29: SandboxService.Delete:output_type -> google.protobuf.Empty + 20, // 30: SandboxService.Pause:output_type -> google.protobuf.Empty + 12, // 31: SandboxService.Checkpoint:output_type -> SandboxCheckpointResponse + 16, // 32: SandboxService.ListCachedBuilds:output_type -> SandboxListCachedBuildsResponse + 26, // [26:33] is the sub-list for method output_type + 19, // [19:26] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_orchestrator_proto_init() } diff --git a/packages/shared/pkg/proxy/errors.go b/packages/shared/pkg/proxy/errors.go index 1492fc0d40..afad4b0278 100644 --- a/packages/shared/pkg/proxy/errors.go +++ b/packages/shared/pkg/proxy/errors.go @@ -82,6 +82,38 @@ func NewErrInvalidTrafficAccessToken(sandboxId string, header string) *InvalidTr } } +type PortNotAllowedError struct { + SandboxId string + Port uint64 +} + +func NewErrPortNotAllowed(sandboxId string, port uint64) *PortNotAllowedError { + return &PortNotAllowedError{ + SandboxId: sandboxId, + Port: port, + } +} + +func (e PortNotAllowedError) Error() string { + return "port not allowed" +} + +type ClientIPNotAllowedError struct { + SandboxId string + ClientIP string +} + +func NewErrClientIPNotAllowed(sandboxId string, clientIP string) *ClientIPNotAllowedError { + return &ClientIPNotAllowedError{ + SandboxId: sandboxId, + ClientIP: clientIP, + } +} + +func (e ClientIPNotAllowedError) Error() string { + return "client IP not allowed" +} + type SandboxResourceExhaustedError struct { SandboxId string Message string diff --git a/packages/shared/pkg/proxy/handler.go b/packages/shared/pkg/proxy/handler.go index f1b059038d..59ce6ad949 100644 --- a/packages/shared/pkg/proxy/handler.go +++ b/packages/shared/pkg/proxy/handler.go @@ -105,6 +105,46 @@ func handler(p *pool.ProxyPool, getDestination func(r *http.Request) (*pool.Dest return } + var portNotAllowedErr *PortNotAllowedError + if errors.As(err, &portNotAllowedErr) { + logger.L().Warn(ctx, "port not allowed", + zap.String("host", r.Host), + logger.WithSandboxID(portNotAllowedErr.SandboxId), + zap.Uint64("port", portNotAllowedErr.Port)) + + err = template. + NewPortNotAllowedError(portNotAllowedErr.SandboxId, r.Host, portNotAllowedErr.Port). + HandleError(w, r) + if err != nil { + logger.L().Error(ctx, "failed to handle port not allowed error", zap.Error(err), logger.WithSandboxID(portNotAllowedErr.SandboxId)) + http.Error(w, "Failed to handle port not allowed error", http.StatusInternalServerError) + + return + } + + return + } + + var clientIPNotAllowedErr *ClientIPNotAllowedError + if errors.As(err, &clientIPNotAllowedErr) { + logger.L().Warn(ctx, "client IP not allowed", + zap.String("host", r.Host), + logger.WithSandboxID(clientIPNotAllowedErr.SandboxId), + zap.String("client_ip", clientIPNotAllowedErr.ClientIP)) + + err = template. + NewClientIPNotAllowedError(clientIPNotAllowedErr.SandboxId, r.Host, clientIPNotAllowedErr.ClientIP). + HandleError(w, r) + if err != nil { + logger.L().Error(ctx, "failed to handle client IP not allowed error", zap.Error(err), logger.WithSandboxID(clientIPNotAllowedErr.SandboxId)) + http.Error(w, "Failed to handle client IP not allowed error", http.StatusInternalServerError) + + return + } + + return + } + var trafficMissingTokenErr *MissingTrafficAccessTokenError if errors.As(err, &trafficMissingTokenErr) { logger.L().Warn(ctx, "traffic access token is missing", zap.String("host", r.Host)) diff --git a/packages/shared/pkg/proxy/proxy.go b/packages/shared/pkg/proxy/proxy.go index e43af29b13..f0e8895f67 100644 --- a/packages/shared/pkg/proxy/proxy.go +++ b/packages/shared/pkg/proxy/proxy.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/http" + "strings" "sync/atomic" "time" @@ -107,3 +108,37 @@ func (p *Proxy) ListenAndServe(ctx context.Context) error { func (p *Proxy) Serve(l net.Listener) error { return p.Server.Serve(tracking.NewListener(l, &p.currentServerConnsCounter)) } + +// ClientIPHeader is an internal header set by client-proxy to forward +// the real client IP to the orchestrator proxy. It is always stripped +// before the request is forwarded to the sandbox. +const ClientIPHeader = "X-E2B-Client-IP" + +// ExtractClientIP returns the real client IP from an HTTP request. +// Priority: X-E2B-Client-IP (internal, set by client-proxy) → X-Forwarded-For → RemoteAddr. +// +// GCP external HTTPS LB appends two entries to X-Forwarded-For: +// +// X-Forwarded-For: , , +// +// We take the second-to-last entry, which is the real client IP added by +// the load balancer. Taking the first entry would be spoofable. + +func ExtractClientIP(r *http.Request) string { + if clientIP := r.Header.Get(ClientIPHeader); clientIP != "" { + return clientIP + } + + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + parts := strings.Split(xff, ",") + + return strings.TrimSpace(parts[max(len(parts)-2, 0)]) + } + + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + + return host +} diff --git a/packages/shared/pkg/proxy/template/browser_client_ip_not_allowed.html b/packages/shared/pkg/proxy/template/browser_client_ip_not_allowed.html new file mode 100644 index 0000000000..ab06ed529a --- /dev/null +++ b/packages/shared/pkg/proxy/template/browser_client_ip_not_allowed.html @@ -0,0 +1,22 @@ + + + + + Access Denied + + + +
+ +
+

Access Denied

+

Your IP address is not allowed to access sandbox {{.SandboxId}}.

+
+
+ {{.Host}} +
Client IP {{.ClientIP}} is blocked by the sandbox network policy
+
+

The sandbox owner has restricted which IP addresses can connect. Contact them if you believe this is an error.

+
+ + \ No newline at end of file diff --git a/packages/shared/pkg/proxy/template/browser_port_not_allowed.html b/packages/shared/pkg/proxy/template/browser_port_not_allowed.html new file mode 100644 index 0000000000..f5ae022971 --- /dev/null +++ b/packages/shared/pkg/proxy/template/browser_port_not_allowed.html @@ -0,0 +1,22 @@ + + + + + Port Not Allowed + + + +
+ +
+

Port Not Allowed

+

Access to port {{.Port}} on sandbox {{.SandboxId}} is not allowed.

+
+
+ {{.Host}} +
Port {{.Port}} is blocked by the sandbox network policy
+
+

The sandbox owner has restricted which ports can be accessed. Contact them if you believe this is an error.

+
+ + \ No newline at end of file diff --git a/packages/shared/pkg/proxy/template/ingress_error.go b/packages/shared/pkg/proxy/template/ingress_error.go new file mode 100644 index 0000000000..a7b41a88c1 --- /dev/null +++ b/packages/shared/pkg/proxy/template/ingress_error.go @@ -0,0 +1,66 @@ +package template + +import ( + _ "embed" + "fmt" + "html/template" + "net/http" +) + +//go:embed browser_port_not_allowed.html +var portNotAllowedHtml string +var portNotAllowedHtmlTemplate = template.Must(template.New("portNotAllowedHtml").Parse(portNotAllowedHtml)) + +//go:embed browser_client_ip_not_allowed.html +var clientIPNotAllowedHtml string +var clientIPNotAllowedHtmlTemplate = template.Must(template.New("clientIPNotAllowedHtml").Parse(clientIPNotAllowedHtml)) + +type portNotAllowedData struct { + SandboxId string `json:"sandboxId"` + Port uint64 `json:"port"` + Message string `json:"message"` + Code int `json:"code"` + Host string `json:"-"` +} + +func (e portNotAllowedData) StatusCode() int { + return e.Code +} + +func NewPortNotAllowedError(sandboxId string, host string, port uint64) *TemplatedError[portNotAllowedData] { + return &TemplatedError[portNotAllowedData]{ + template: portNotAllowedHtmlTemplate, + vars: portNotAllowedData{ + SandboxId: sandboxId, + Port: port, + Message: fmt.Sprintf("Access to port %d is not allowed", port), + Host: host, + Code: http.StatusForbidden, + }, + } +} + +type clientIPNotAllowedData struct { + SandboxId string `json:"sandboxId"` + ClientIP string `json:"clientIp"` + Message string `json:"message"` + Code int `json:"code"` + Host string `json:"-"` +} + +func (e clientIPNotAllowedData) StatusCode() int { + return e.Code +} + +func NewClientIPNotAllowedError(sandboxId string, host string, clientIP string) *TemplatedError[clientIPNotAllowedData] { + return &TemplatedError[clientIPNotAllowedData]{ + template: clientIPNotAllowedHtmlTemplate, + vars: clientIPNotAllowedData{ + SandboxId: sandboxId, + ClientIP: clientIP, + Message: "Your IP address is not allowed to access this sandbox", + Host: host, + Code: http.StatusForbidden, + }, + } +} diff --git a/spec/openapi.yml b/spec/openapi.yml index 941e873ef9..8fb5ac31ff 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -282,6 +282,30 @@ components: maskRequestHost: type: string description: Specify host mask which will be used for all sandbox requests + allowPorts: + type: array + description: List of ports accessible from outside the sandbox. If set, only these ports accept inbound HTTP connections. Allowed ports always take precedence over denied ports. + items: + type: integer + minimum: 1 + maximum: 65535 + denyPorts: + type: array + description: List of ports blocked from outside access. Ignored if allowPorts is set. + items: + type: integer + minimum: 1 + maximum: 65535 + allowIn: + type: array + description: List of client IP CIDRs allowed to reach the sandbox (e.g. "203.0.113.0/24"). If set, only matching clients can connect. Allowed entries always take precedence over denied entries. + items: + type: string + denyIn: + type: array + description: List of client IP CIDRs denied from reaching the sandbox. + items: + type: string SandboxAutoResumeEnabled: type: boolean @@ -2334,7 +2358,7 @@ paths: /sandboxes/{sandboxID}/network: put: - description: Update the network configuration for a running sandbox. Replaces the current egress rules with the provided configuration. Omitting both fields clears all egress rules. + description: Update the network configuration for a running sandbox. Replaces the current network rules with the provided configuration. Omitting fields clears those rules. security: - ApiKeyAuth: [] - Supabase1TokenAuth: [] @@ -2357,6 +2381,34 @@ paths: description: List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. items: type: string + allowPorts: + type: array + description: List of ports accessible from outside the sandbox. If set, only these ports accept inbound HTTP connections. Allowed ports always take precedence over denied ports. + items: + type: integer + minimum: 1 + maximum: 65535 + denyPorts: + type: array + description: List of ports blocked from outside access. Ignored if allowPorts is set. + items: + type: integer + minimum: 1 + maximum: 65535 + allowIn: + type: array + description: List of client IP CIDRs allowed to reach the sandbox (e.g. "203.0.113.0/24"). If set, only matching clients can connect. Allowed entries always take precedence over denied entries. + items: + type: string + denyIn: + type: array + description: List of client IP CIDRs denied from reaching the sandbox. + items: + type: string + maskRequestHost: + type: string + nullable: true + description: Specify host mask which will be used for all sandbox requests. Set to null to clear. parameters: - $ref: "#/components/parameters/sandboxID" responses: diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index e1b1f028aa..f81d07247a 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -709,15 +709,27 @@ type SandboxMetric struct { // SandboxNetworkConfig defines model for SandboxNetworkConfig. type SandboxNetworkConfig struct { + // AllowIn List of client IP CIDRs allowed to reach the sandbox (e.g. "203.0.113.0/24"). If set, only matching clients can connect. Allowed entries always take precedence over denied entries. + AllowIn *[]string `json:"allowIn,omitempty"` + // AllowOut List of allowed destinations for egress traffic. Each entry can be a CIDR block (e.g. "8.8.8.8/32"), a bare IP address (e.g. "8.8.8.8"), or a domain name (e.g. "example.com", "*.example.com"). Allowed entries always take precedence over denied entries. AllowOut *[]string `json:"allowOut,omitempty"` + // AllowPorts List of ports accessible from outside the sandbox. If set, only these ports accept inbound HTTP connections. Allowed ports always take precedence over denied ports. + AllowPorts *[]int `json:"allowPorts,omitempty"` + // AllowPublicTraffic Specify if the sandbox URLs should be accessible only with authentication. AllowPublicTraffic *bool `json:"allowPublicTraffic,omitempty"` + // DenyIn List of client IP CIDRs denied from reaching the sandbox. + DenyIn *[]string `json:"denyIn,omitempty"` + // DenyOut List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. DenyOut *[]string `json:"denyOut,omitempty"` + // DenyPorts List of ports blocked from outside access. Ignored if allowPorts is set. + DenyPorts *[]int `json:"denyPorts,omitempty"` + // MaskRequestHost Specify host mask which will be used for all sandbox requests MaskRequestHost *string `json:"maskRequestHost,omitempty"` } @@ -1262,11 +1274,26 @@ type GetSandboxesSandboxIDMetricsParams struct { // PutSandboxesSandboxIDNetworkJSONBody defines parameters for PutSandboxesSandboxIDNetwork. type PutSandboxesSandboxIDNetworkJSONBody struct { + // AllowIn List of client IP CIDRs allowed to reach the sandbox (e.g. "203.0.113.0/24"). If set, only matching clients can connect. Allowed entries always take precedence over denied entries. + AllowIn *[]string `json:"allowIn,omitempty"` + // AllowOut List of allowed destinations for egress traffic. Each entry can be a CIDR block (e.g. "8.8.8.8/32"), a bare IP address (e.g. "8.8.8.8"), or a domain name (e.g. "example.com", "*.example.com"). Allowed entries always take precedence over denied entries. AllowOut *[]string `json:"allowOut,omitempty"` + // AllowPorts List of ports accessible from outside the sandbox. If set, only these ports accept inbound HTTP connections. Allowed ports always take precedence over denied ports. + AllowPorts *[]int `json:"allowPorts,omitempty"` + + // DenyIn List of client IP CIDRs denied from reaching the sandbox. + DenyIn *[]string `json:"denyIn,omitempty"` + // DenyOut List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. DenyOut *[]string `json:"denyOut,omitempty"` + + // DenyPorts List of ports blocked from outside access. Ignored if allowPorts is set. + DenyPorts *[]int `json:"denyPorts,omitempty"` + + // MaskRequestHost Specify host mask which will be used for all sandbox requests. Set to null to clear. + MaskRequestHost *string `json:"maskRequestHost"` } // PostSandboxesSandboxIDRefreshesJSONBody defines parameters for PostSandboxesSandboxIDRefreshes. diff --git a/tests/integration/internal/tests/api/sandboxes/sandbox_network_update_test.go b/tests/integration/internal/tests/api/sandboxes/sandbox_network_update_test.go index 07a458b17c..20a054822a 100644 --- a/tests/integration/internal/tests/api/sandboxes/sandbox_network_update_test.go +++ b/tests/integration/internal/tests/api/sandboxes/sandbox_network_update_test.go @@ -2,11 +2,18 @@ package sandboxes import ( "context" + "fmt" + "io" "net/http" + "net/url" + "strings" "testing" + "time" "github.com/stretchr/testify/require" + "github.com/e2b-dev/infra/packages/shared/pkg/consts" + "github.com/e2b-dev/infra/packages/shared/pkg/proxy/pool" sandbox_network "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network" "github.com/e2b-dev/infra/tests/integration/internal/api" "github.com/e2b-dev/infra/tests/integration/internal/setup" @@ -61,9 +68,9 @@ func verifyConnectivity( } } -// TestUpdateNetworkConfig exercises all update scenarios using a single sandbox. +// TestUpdateEgressConfig exercises all update scenarios using a single sandbox. // Subtests run sequentially — each PUT fully replaces the previous config. -func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are sequential +func TestUpdateEgressConfig(t *testing.T) { //nolint:tparallel // subtests are sequential t.Parallel() templateID := ensureNetworkTestTemplate(t) @@ -181,7 +188,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are steps := []step{ // ── deny-only rules ────────────────────────────────────────── { - name: "1_deny_all_blocks_everything", + name: "deny_all_blocks_everything", denyOut: ptrS(blockAll), checks: []connectivityCheck{ {"https://8.8.8.8", false}, @@ -190,7 +197,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── allow + deny-all (allow takes precedence over deny) ────── { - name: "2_allow_single_ip_through_deny_all", + name: "allow_single_ip_through_deny_all", allowOut: ptrS("8.8.8.8"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -199,7 +206,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, }, { - name: "3_replace_allowed_ip", + name: "replace_allowed_ip", allowOut: ptrS("1.1.1.1"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -208,7 +215,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, }, { - name: "4_allow_multiple_ips", + name: "allow_multiple_ips", allowOut: ptrS("8.8.8.8", "1.1.1.1"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -217,7 +224,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, }, { - name: "5_allow_cidr_range", + name: "allow_cidr_range", allowOut: ptrS("8.8.8.0/24"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -227,7 +234,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── domain-based rules (TCP proxy SNI matching) ────────────── { - name: "6_allow_domain", + name: "allow_domain", allowOut: ptrS("google.com"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -236,7 +243,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, }, { - name: "7_allow_domain_and_ip", + name: "allow_domain_and_ip", allowOut: ptrS("google.com", "1.1.1.1"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -247,7 +254,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── replacement semantics: PUT replaces, not appends ───────── { - name: "8_remove_allow_keep_deny", + name: "remove_allow_keep_deny", denyOut: ptrS(blockAll), checks: []connectivityCheck{ {"https://google.com", false}, // previously allowed domain now blocked @@ -256,7 +263,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── clear all rules: back to default-allow ─────────────────── { - name: "9_clear_all_rules_restores_access", + name: "clear_all_restores_access", checks: []connectivityCheck{ {"https://8.8.8.8", true}, {"https://1.1.1.1", true}, @@ -264,7 +271,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── re-apply after clear: sets can be repopulated ──────────── { - name: "10_reapply_rules_after_clear", + name: "reapply_after_clear", allowOut: ptrS("1.1.1.1"), denyOut: ptrS(blockAll), checks: []connectivityCheck{ @@ -274,7 +281,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── allow IP without deny: no blocking, allow set is no-op ─── { - name: "11_allow_ip_without_deny_no_blocking", + name: "allow_ip_without_deny_no_blocking", allowOut: ptrS("8.8.8.8"), checks: []connectivityCheck{ {"https://8.8.8.8", true}, @@ -283,7 +290,7 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }, // ── final clear ────────────────────────────────────────────── { - name: "12_final_clear", + name: "final_clear", checks: []connectivityCheck{ {"https://8.8.8.8", true}, {"https://1.1.1.1", true}, @@ -336,3 +343,404 @@ func TestUpdateNetworkConfig(t *testing.T) { //nolint:tparallel // subtests are }) }) } + +// ============================================================================= +// TestUpdateIngressConfig exercises ingress control (port + client IP filtering) +// via the PUT /sandboxes/{sandboxID}/network endpoint. +// Subtests run sequentially — each PUT fully replaces the previous config. +// ============================================================================= + +// ingressRules is a builder for ingress config updates. +type ingressRules struct { + body api.PutSandboxesSandboxIDNetworkJSONRequestBody +} + +func ingress() *ingressRules { return &ingressRules{} } +func (r *ingressRules) denyPorts(ports ...int) *ingressRules { + r.body.DenyPorts = &ports + + return r +} + +func (r *ingressRules) allowPorts(ports ...int) *ingressRules { + r.body.AllowPorts = &ports + + return r +} + +func (r *ingressRules) denyIn(cidrs ...string) *ingressRules { + r.body.DenyIn = &cidrs + + return r +} + +func (r *ingressRules) allowIn(cidrs ...string) *ingressRules { + r.body.AllowIn = &cidrs + + return r +} + +func (r *ingressRules) maskHost(h string) *ingressRules { + r.body.MaskRequestHost = &h + + return r +} + +func TestUpdateIngressConfig(t *testing.T) { //nolint:tparallel // subtests are sequential + t.Parallel() + + ctx := t.Context() + client := setup.GetAPIClient() + envdClient := setup.GetEnvdClient(t, ctx) + + sbx := utils.SetupSandboxWithCleanup(t, client, + utils.WithTimeout(120), + utils.WithAutoPause(false), + ) + + testPort := 8000 + proxyURL, err := url.Parse(setup.EnvdProxy) + require.NoError(t, err) + httpClient := &http.Client{Timeout: 10 * time.Second} + + apply := func(r *ingressRules) { + t.Helper() + resp := putNetwork(t, ctx, client, sbx.SandboxID, r.body) + require.Equal(t, http.StatusNoContent, resp.StatusCode()) + } + + proxyStatus := func(port int, fromIP string) int { + t.Helper() + var headers *http.Header + if fromIP != "" { + // Spoof via X-Forwarded-For so the request flows through the real + // client-proxy → orchestrator-proxy path. Client-proxy strips any + // incoming X-E2B-Client-IP and derives the client IP from XFF. + // ExtractClientIP takes the second-to-last XFF entry (the one a + // real GCP LB would append), so we add a dummy trailing entry. + headers = &http.Header{"X-Forwarded-For": []string{fromIP + ", 0.0.0.0"}} + } + req := utils.NewRequest(sbx, proxyURL, port, headers) + resp, err := httpClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + + return resp.StatusCode + } + + // Start HTTP servers so the proxy connects immediately instead of retrying. + for _, p := range []int{testPort, testPort + 1} { + err = utils.ExecCommand(t, ctx, sbx, envdClient, "sh", "-c", + fmt.Sprintf("nohup python3 -m http.server %d >/dev/null 2>&1 &", p)) + require.NoError(t, err) + } + + // Each check: port to hit, optional spoofed IP, whether we expect 403. + type check struct { + port int + fromIP string + blocked bool + } + + type step struct { + name string + rules *ingressRules + checks []check + ciOnly bool // skip when running against GCP (client-proxy overwrites spoofed IP) + } + + envdPort := int(consts.DefaultEnvdServerPort) + isCI := strings.Contains(setup.EnvdProxy, "localhost") || strings.Contains(setup.EnvdProxy, "127.0.0.1") + + steps := []step{ + // ── Port deny/allow ────────────────────────────────────────── + { + name: "port_deny_blocks_access", + rules: ingress().denyPorts(testPort), + checks: []check{ + {testPort, "", true}, + {testPort + 1, "", false}, + }, + }, + { + name: "port_allow_overrides_deny", + rules: ingress().allowPorts(testPort).denyPorts(testPort), + checks: []check{ + {testPort, "", false}, + }, + }, + // ── Client IP deny/allow ───────────────────────────────────── + { + // Both IPv4 and IPv6 "match all" CIDRs — CI may use ::1 (IPv6 loopback). + name: "client_ip_deny_all_blocks", + rules: ingress().denyIn("0.0.0.0/0", "::/0"), + checks: []check{ + {testPort, "", true}, + }, + }, + { + name: "client_ip_allow_all_overrides_deny_all", + rules: ingress().allowIn("0.0.0.0/0", "::/0").denyIn("0.0.0.0/0", "::/0"), + checks: []check{ + {testPort, "", false}, + }, + }, + { + // Deny a reserved TEST-NET range (198.51.100.0/24, RFC 5737) that no real + // machine uses. Our real IP won't match → request goes through. + name: "client_ip_deny_narrow_cidr_does_not_block_us", + rules: ingress().denyIn("198.51.100.0/24"), + checks: []check{ + {testPort, "", false}, + }, + }, + { + // Deny both halves of IPv4 + IPv6 space to cover every possible real IP. + name: "client_ip_deny_both_halves_blocks", + rules: ingress().denyIn("0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1"), + checks: []check{ + {testPort, "", true}, + }, + }, + { + name: "client_ip_allow_overrides_deny_all", + rules: ingress().allowIn("0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1").denyIn("0.0.0.0/0", "::/0"), + checks: []check{ + {testPort, "", false}, + }, + }, + // ── Spoofed X-E2B-Client-IP (CI-only, bypass client-proxy) ── + { + name: "spoofed_ip_deny_specific_cidr_blocks", + ciOnly: true, + rules: ingress().denyIn("203.0.113.0/24"), + checks: []check{ + {testPort, "203.0.113.42", true}, + {testPort, "198.51.100.1", false}, + }, + }, + { + name: "spoofed_ip_allow_overrides_deny", + ciOnly: true, + rules: ingress().allowIn("203.0.113.42/32").denyIn("0.0.0.0/0", "203.0.113.0/24"), + checks: []check{ + {testPort, "203.0.113.42", false}, + {testPort, "203.0.113.99", true}, + }, + }, + // ── Envd port exempt from both port deny and client IP deny ── + { + name: "envd_exempt_from_ingress_restrictions", + rules: ingress().denyPorts(envdPort).denyIn("0.0.0.0/0"), + checks: []check{ + {envdPort, "", false}, + }, + }, + // ── Empty PUT clears ingress rules ─────────────────────────── + { + name: "clear_restores_access", + rules: ingress(), + checks: []check{ + {testPort, "", false}, + }, + }, + } + + for _, s := range steps { //nolint:paralleltest // sequential + if s.ciOnly && !isCI { + continue + } + t.Run(s.name, func(t *testing.T) { + apply(s.rules) + for _, c := range s.checks { + got := proxyStatus(c.port, c.fromIP) + if c.blocked { + require.Equal(t, http.StatusForbidden, got, "port=%d fromIP=%q should be blocked", c.port, c.fromIP) + } else { + require.NotEqual(t, http.StatusForbidden, got, "port=%d fromIP=%q should not be blocked", c.port, c.fromIP) + } + } + }) + } + + // ── Pause/resume preserves ingress rules (must be last) ───────────── + + t.Run("pause_resume_preserves_ingress_rules", func(t *testing.T) { + apply(ingress().denyPorts(testPort)) + require.Equal(t, http.StatusForbidden, proxyStatus(testPort, "")) + + pauseResp, err := client.PostSandboxesSandboxIDPauseWithResponse(ctx, sbx.SandboxID, setup.WithAPIKey()) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, pauseResp.StatusCode()) + + resumeResp, err := client.PostSandboxesSandboxIDResumeWithResponse(ctx, sbx.SandboxID, + api.PostSandboxesSandboxIDResumeJSONRequestBody{}, + setup.WithAPIKey(), + ) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resumeResp.StatusCode()) + + require.Equal(t, http.StatusForbidden, proxyStatus(testPort, "")) + }) +} + +// ============================================================================= +// TestUpdateCombinedEgressAndIngress verifies that egress and ingress rules +// can be set in a single PUT and both take effect simultaneously. +// ============================================================================= + +func TestUpdateCombinedEgressAndIngress(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client := setup.GetAPIClient() + envdClient := setup.GetEnvdClient(t, ctx) + + sbx := utils.SetupSandboxWithCleanup(t, client, + utils.WithTimeout(120), + utils.WithAutoPause(false), + ) + + testPort := 8000 + proxyURL, err := url.Parse(setup.EnvdProxy) + require.NoError(t, err) + httpClient := &http.Client{Timeout: 10 * time.Second} + + // Start an HTTP server so the proxy connects. + err = utils.ExecCommand(t, ctx, sbx, envdClient, "sh", "-c", + fmt.Sprintf("nohup python3 -m http.server %d >/dev/null 2>&1 &", testPort)) + require.NoError(t, err) + + // Wait for the server to be reachable. + waitResp := utils.WaitForStatus(t, httpClient, sbx, proxyURL, testPort, nil, http.StatusOK) + waitResp.Body.Close() + + // Single PUT: deny all egress + deny port for ingress. + resp := putNetwork(t, ctx, client, sbx.SandboxID, api.PutSandboxesSandboxIDNetworkJSONRequestBody{ + DenyOut: ptrS(blockAll), + DenyPorts: &[]int{testPort}, + }) + require.Equal(t, http.StatusNoContent, resp.StatusCode()) + + // Egress: outbound blocked. + verifyConnectivity(t, ctx, sbx, envdClient, []connectivityCheck{ + {"https://8.8.8.8", false}, + }) + + // Ingress: port blocked. + req := utils.NewRequest(sbx, proxyURL, testPort, nil) + proxyResp, err := httpClient.Do(req) + require.NoError(t, err) + proxyResp.Body.Close() + require.Equal(t, http.StatusForbidden, proxyResp.StatusCode) + + // Clear both: empty body restores defaults. + resp = putNetwork(t, ctx, client, sbx.SandboxID, api.PutSandboxesSandboxIDNetworkJSONRequestBody{}) + require.Equal(t, http.StatusNoContent, resp.StatusCode()) + + // Egress: outbound restored. + verifyConnectivity(t, ctx, sbx, envdClient, []connectivityCheck{ + {"https://8.8.8.8", true}, + }) + + // Ingress: port restored. + req = utils.NewRequest(sbx, proxyURL, testPort, nil) + proxyResp, err = httpClient.Do(req) + require.NoError(t, err) + proxyResp.Body.Close() + require.NotEqual(t, http.StatusForbidden, proxyResp.StatusCode) +} + +// ============================================================================= +// TestUpdateMaskRequestHost exercises dynamic MaskRequestHost updates. +// A Python server echoes the Host header back so we can verify masking. +// ============================================================================= + +func TestUpdateMaskRequestHost(t *testing.T) { //nolint:tparallel // subtests are sequential + t.Parallel() + + ctx := t.Context() + client := setup.GetAPIClient() + envdClient := setup.GetEnvdClient(t, ctx) + + sbx := utils.SetupSandboxWithCleanup(t, client, + utils.WithTimeout(120), + utils.WithAutoPause(false), + ) + + testPort := 8000 + proxyURL, err := url.Parse(setup.EnvdProxy) + require.NoError(t, err) + httpClient := &http.Client{Timeout: 10 * time.Second} + + // Start a Python server that echoes the Host header in the response body. + echoServer := fmt.Sprintf(` +import http.server, socketserver +class H(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.end_headers() + self.wfile.write(self.headers.get("Host","").encode()) + def log_message(self, *a): pass +socketserver.TCPServer(("", %d), H).serve_forever() +`, testPort) + err = utils.ExecCommand(t, ctx, sbx, envdClient, "sh", "-c", + "nohup python3 -c '"+echoServer+"' >/dev/null 2>&1 &") + require.NoError(t, err) + + // Wait for the echo server to be ready. + resp := utils.WaitForStatus(t, httpClient, sbx, proxyURL, testPort, nil, http.StatusOK) + resp.Body.Close() + + // Returns the Host header as seen by the server inside the sandbox. + getHost := func() string { + t.Helper() + req := utils.NewRequest(sbx, proxyURL, testPort, nil) + resp, err := httpClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + return string(body) + } + + apply := func(r *ingressRules) { + t.Helper() + resp := putNetwork(t, ctx, client, sbx.SandboxID, r.body) + require.Equal(t, http.StatusNoContent, resp.StatusCode()) + } + + // Verify baseline — Host should contain the sandbox routing domain, not a mask. + t.Run("baseline_no_mask", func(t *testing.T) { //nolint:paralleltest // sequential + host := getHost() + require.NotEmpty(t, host) + require.NotContains(t, host, "masked-host") + }) + + maskedTemplate := fmt.Sprintf("masked-host:%s", pool.MaskRequestHostPortPlaceholder) + maskedExpected := fmt.Sprintf("masked-host:%d", testPort) + + t.Run("set_mask_with_port_placeholder", func(t *testing.T) { //nolint:paralleltest // sequential + apply(ingress().maskHost(maskedTemplate)) + require.Equal(t, maskedExpected, getHost()) + }) + + t.Run("update_mask", func(t *testing.T) { //nolint:paralleltest // sequential + apply(ingress().maskHost("other-host:9999")) + require.Equal(t, "other-host:9999", getHost()) + }) + + t.Run("clear_mask", func(t *testing.T) { //nolint:paralleltest // sequential + // Empty ingress() sets MaskRequestHost to nil — clears the mask. + apply(ingress()) + host := getHost() + require.NotEqual(t, "other-host:9999", host) + require.NotContains(t, host, "masked-host") + }) + + t.Run("set_mask_again", func(t *testing.T) { //nolint:paralleltest // sequential + apply(ingress().maskHost(maskedTemplate)) + require.Equal(t, maskedExpected, getHost()) + }) +}