Skip to content

Commit 2eb8be2

Browse files
committed
feat: add host mask and public access auth
1 parent 7f6e9dc commit 2eb8be2

File tree

14 files changed

+448
-11
lines changed

14 files changed

+448
-11
lines changed

.changeset/cuddly-horses-push.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@e2b/python-sdk': minor
3+
'e2b': minor
4+
---
5+
6+
add possibility to mask the Host in public requests with custom value

.changeset/loud-bottles-provide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@e2b/python-sdk': minor
3+
'e2b': minor
4+
---
5+
6+
add ability to secure public traffic using token

packages/js-sdk/src/sandbox/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ export class Sandbox extends SandboxApi {
9090
*/
9191
readonly sandboxDomain: string
9292

93+
/**
94+
* Traffic access token for accessing sandbox services with restricted public traffic.
95+
*/
96+
readonly trafficAccessToken?: string
97+
9398
protected readonly envdPort = 49983
9499
protected readonly mcpPort = 50005
95100

@@ -113,6 +118,7 @@ export class Sandbox extends SandboxApi {
113118
sandboxDomain?: string
114119
envdVersion: string
115120
envdAccessToken?: string
121+
trafficAccessToken?: string
116122
}
117123
) {
118124
super()
@@ -123,6 +129,7 @@ export class Sandbox extends SandboxApi {
123129
this.sandboxDomain = opts.sandboxDomain ?? this.connectionConfig.domain
124130

125131
this.envdAccessToken = opts.envdAccessToken
132+
this.trafficAccessToken = opts.trafficAccessToken
126133
this.envdApiUrl = `${
127134
this.connectionConfig.debug ? 'http' : 'https'
128135
}://${this.getHost(this.envdPort)}`
@@ -411,6 +418,7 @@ export class Sandbox extends SandboxApi {
411418
sandboxId,
412419
sandboxDomain: sandbox.sandboxDomain,
413420
envdAccessToken: sandbox.envdAccessToken,
421+
trafficAccessToken: sandbox.trafficAccessToken,
414422
envdVersion: sandbox.envdVersion,
415423
...config,
416424
}) as InstanceType<S>

packages/js-sdk/src/sandbox/sandboxApi.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ export type SandboxNetworkOpts = {
4949
* - To deny traffic to a specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
5050
*/
5151
denyOut?: string[]
52+
53+
/**
54+
* Specify if the sandbox URLs should be accessible only with authentication.
55+
* @default true
56+
*/
57+
allowPublicTraffic?: boolean
58+
59+
/** Specify host mask which will be used for all sandbox requests in the header.
60+
* You can use the ${PORT} variable that will be replaced with the actual port number of the service.
61+
*
62+
* @default ${PORT}-sandboxid.e2b.app
63+
*/
64+
maskRequestHost?: string
5265
}
5366

5467
/**
@@ -547,6 +560,7 @@ export class SandboxApi {
547560
sandboxDomain: res.data!.domain || undefined,
548561
envdVersion: res.data!.envdVersion,
549562
envdAccessToken: res.data!.envdAccessToken,
563+
trafficAccessToken: res.data!.trafficAccessToken || undefined,
550564
}
551565
}
552566

@@ -585,6 +599,7 @@ export class SandboxApi {
585599
sandboxDomain: res.data!.domain || undefined,
586600
envdVersion: res.data!.envdVersion,
587601
envdAccessToken: res.data!.envdAccessToken,
602+
trafficAccessToken: res.data!.trafficAccessToken || undefined,
588603
}
589604
}
590605
}

packages/js-sdk/tests/sandbox/network.test.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assert, expect } from 'vitest'
1+
import { assert, expect, describe } from 'vitest'
22

33
import { CommandExitError, ALL_TRAFFIC } from '../../src'
44
import { sandboxTest, isDebug } from '../setup.js'
@@ -105,3 +105,130 @@ sandboxTest
105105
assert.equal(result2.exitCode, 0)
106106
assert.equal(result2.stdout.trim(), '301')
107107
})
108+
109+
describe('allowPublicTraffic=false', () => {
110+
sandboxTest.scoped({
111+
sandboxOpts: {
112+
network: {
113+
allowPublicTraffic: false,
114+
},
115+
},
116+
})
117+
118+
sandboxTest.skipIf(isDebug)(
119+
'sandbox requires traffic access token',
120+
async ({ sandbox }) => {
121+
// Verify the sandbox was created successfully and has a traffic access token
122+
assert(sandbox.trafficAccessToken)
123+
124+
// Start a simple HTTP server in the sandbox
125+
const port = 8080
126+
sandbox.commands.run(`python3 -m http.server ${port}`, {
127+
background: true,
128+
})
129+
130+
// Wait for server to start
131+
await new Promise((resolve) => setTimeout(resolve, 3000))
132+
133+
// Get the public URL for the sandbox
134+
const sandboxUrl = `https://${sandbox.getHost(port)}`
135+
136+
// Test 1: Request without traffic access token should fail with 403
137+
const response1 = await fetch(sandboxUrl)
138+
assert.equal(response1.status, 403)
139+
140+
// Test 2: Request with valid traffic access token should succeed
141+
const response2 = await fetch(sandboxUrl, {
142+
headers: {
143+
'e2b-traffic-access-token': sandbox.trafficAccessToken,
144+
},
145+
})
146+
assert.equal(response2.status, 200)
147+
}
148+
)
149+
})
150+
151+
describe('allowPublicTraffic=true', () => {
152+
sandboxTest.scoped({
153+
sandboxOpts: {
154+
network: {
155+
allowPublicTraffic: true,
156+
},
157+
},
158+
})
159+
160+
sandboxTest.skipIf(isDebug)(
161+
'sandbox works without token',
162+
async ({ sandbox }) => {
163+
// Start a simple HTTP server in the sandbox
164+
const port = 8080
165+
sandbox.commands.run(`python3 -m http.server ${port}`, {
166+
background: true,
167+
})
168+
169+
// Wait for server to start
170+
await new Promise((resolve) => setTimeout(resolve, 3000))
171+
172+
// Get the public URL for the sandbox
173+
const sandboxUrl = `https://${sandbox.getHost(port)}`
174+
175+
// Request without traffic access token should succeed (public access enabled)
176+
const response = await fetch(sandboxUrl)
177+
assert.equal(response.status, 200)
178+
}
179+
)
180+
})
181+
182+
describe('maskRequestHost option', () => {
183+
sandboxTest.scoped({
184+
sandboxOpts: {
185+
network: {
186+
maskRequestHost: 'custom-host.example.com:${PORT}',
187+
},
188+
},
189+
})
190+
191+
sandboxTest.skipIf(isDebug)(
192+
'verify maskRequestHost modifies Host header correctly',
193+
async ({ sandbox }) => {
194+
// Install netcat for testing
195+
await sandbox.commands.run('apt-get update', { user: 'root' })
196+
await sandbox.commands.run('apt-get install -y netcat-traditional', {
197+
user: 'root',
198+
})
199+
200+
const port = 8080
201+
const outputFile = '/tmp/nc_output.txt'
202+
203+
// Start netcat listener in background to capture request headers
204+
sandbox.commands.run(`nc -l -p ${port} > ${outputFile}`, {
205+
background: true,
206+
user: 'root',
207+
})
208+
209+
// Wait for netcat to start
210+
await new Promise((resolve) => setTimeout(resolve, 3000))
211+
212+
// Get the public URL for the sandbox
213+
const sandboxUrl = `https://${sandbox.getHost(port)}`
214+
215+
// Make a request from OUTSIDE the sandbox through the proxy
216+
// The Host header should be modified according to maskRequestHost
217+
try {
218+
await fetch(sandboxUrl, { signal: AbortSignal.timeout(5000) })
219+
} catch (error) {
220+
// Request may fail since netcat doesn't respond properly, but headers are captured
221+
}
222+
223+
// Read the captured output from inside the sandbox
224+
const result = await sandbox.commands.run(`cat ${outputFile}`, {
225+
user: 'root',
226+
})
227+
228+
// Verify the Host header was modified according to maskRequestHost
229+
assert.include(result.stdout, 'Host:')
230+
assert.include(result.stdout, 'custom-host.example.com')
231+
assert.include(result.stdout, `${port}`)
232+
}
233+
)
234+
})

packages/python-sdk/e2b/api/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
from types import TracebackType
21
import json
32
import logging
4-
from typing import Optional
5-
from httpx import Limits
63
from dataclasses import dataclass
4+
from types import TracebackType
5+
from typing import Optional
76

7+
from httpx import Limits
88

99
from e2b.api.client.client import AuthenticatedClient
10-
from e2b.connection_config import ConnectionConfig
10+
from e2b.api.client.types import Response
1111
from e2b.api.metadata import default_headers
12+
from e2b.connection_config import ConnectionConfig
1213
from e2b.exceptions import (
1314
AuthenticationException,
14-
SandboxException,
1515
RateLimitException,
16+
SandboxException,
1617
)
17-
from e2b.api.client.types import Response
1818

1919
logger = logging.getLogger(__name__)
2020

@@ -25,6 +25,7 @@ class SandboxCreateResponse:
2525
sandbox_domain: Optional[str]
2626
envd_version: str
2727
envd_access_token: str
28+
traffic_access_token: Optional[str]
2829

2930

3031
def handle_api_exception(

packages/python-sdk/e2b/sandbox/main.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import urllib.parse
2-
from packaging.version import Version
3-
42
from typing import Optional, TypedDict
53

6-
from e2b.sandbox.signature import get_signature
4+
from httpx import Limits
5+
from packaging.version import Version
6+
77
from e2b.connection_config import ConnectionConfig, default_username
88
from e2b.envd.api import ENVD_API_FILES_ROUTE
99
from e2b.envd.versions import ENVD_DEFAULT_USER
10-
from httpx import Limits
10+
from e2b.sandbox.signature import get_signature
1111

1212

1313
class SandboxOpts(TypedDict):
1414
sandbox_id: str
1515
sandbox_domain: Optional[str]
1616
envd_version: Version
1717
envd_access_token: Optional[str]
18+
traffic_access_token: Optional[str]
1819
connection_config: ConnectionConfig
1920

2021

@@ -40,12 +41,14 @@ def __init__(
4041
envd_access_token: Optional[str],
4142
sandbox_domain: Optional[str],
4243
connection_config: ConnectionConfig,
44+
traffic_access_token: Optional[str] = None,
4345
):
4446
self.__connection_config = connection_config
4547
self.__sandbox_id = sandbox_id
4648
self.__sandbox_domain = sandbox_domain or self.connection_config.domain
4749
self.__envd_version = envd_version
4850
self.__envd_access_token = envd_access_token
51+
self.__traffic_access_token = traffic_access_token
4952
self.__envd_api_url = f"{'http' if self.connection_config.debug else 'https'}://{self.get_host(self.envd_port)}"
5053
self.__mcp_token: Optional[str] = None
5154

@@ -70,6 +73,10 @@ def connection_config(self) -> ConnectionConfig:
7073
def _envd_version(self) -> Version:
7174
return self.__envd_version
7275

76+
@property
77+
def traffic_access_token(self) -> Optional[str]:
78+
return self.__traffic_access_token
79+
7380
@property
7481
def sandbox_domain(self) -> Optional[str]:
7582
return self.__sandbox_domain

packages/python-sdk/e2b/sandbox/sandbox_api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ class SandboxNetworkOpts(TypedDict):
6060
- To deny traffic to specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
6161
"""
6262

63+
allow_public_traffic: NotRequired[bool]
64+
"""
65+
Controls whether sandbox URLs should be publicly accessible or require authentication.
66+
Defaults to True.
67+
"""
68+
69+
mask_request_host: NotRequired[str]
70+
"""
71+
Allows specifying a custom host mask for all sandbox requests.
72+
Supports ${PORT} variable. Defaults to "${PORT}-sandboxid.e2b.app".
73+
74+
Examples:
75+
- Custom subdomain: `"${PORT}-myapp.example.com"`
76+
"""
77+
6378

6479
@dataclass
6580
class SandboxInfo:

packages/python-sdk/e2b/sandbox_async/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ async def _cls_connect(
676676
sandbox_domain=sandbox.domain,
677677
envd_version=Version(sandbox.envd_version),
678678
envd_access_token=envd_access_token,
679+
traffic_access_token=sandbox.traffic_access_token,
679680
connection_config=connection_config,
680681
)
681682

@@ -701,6 +702,7 @@ async def _create(
701702
sandbox_domain = None
702703
envd_version = ENVD_DEBUG_FALLBACK
703704
envd_access_token = None
705+
traffic_access_token = None
704706
else:
705707
response = await SandboxApi._create_sandbox(
706708
template=template or cls.default_template,
@@ -719,6 +721,7 @@ async def _create(
719721
sandbox_domain = response.sandbox_domain
720722
envd_version = Version(response.envd_version)
721723
envd_access_token = response.envd_access_token
724+
traffic_access_token = response.traffic_access_token
722725

723726
if envd_access_token is not None and not isinstance(
724727
envd_access_token, Unset
@@ -735,5 +738,6 @@ async def _create(
735738
sandbox_domain=sandbox_domain,
736739
envd_version=envd_version,
737740
envd_access_token=envd_access_token,
741+
traffic_access_token=traffic_access_token,
738742
connection_config=connection_config,
739743
)

packages/python-sdk/e2b/sandbox_async/sandbox_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ async def _create_sandbox(
211211
sandbox_domain=res.parsed.domain,
212212
envd_version=res.parsed.envd_version,
213213
envd_access_token=res.parsed.envd_access_token,
214+
traffic_access_token=res.parsed.traffic_access_token,
214215
)
215216

216217
@classmethod

0 commit comments

Comments
 (0)