Skip to content

Commit 26f913f

Browse files
committed
feat: add host mask and public access auth
1 parent dceb2bc commit 26f913f

File tree

14 files changed

+447
-10
lines changed

14 files changed

+447
-10
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 = this.connectionConfig.getSandboxUrl(this.sandboxId, {
127134
sandboxDomain: this.sandboxDomain,
128135
envdPort: this.envdPort,
@@ -420,6 +427,7 @@ export class Sandbox extends SandboxApi {
420427
sandboxId,
421428
sandboxDomain: sandbox.sandboxDomain,
422429
envdAccessToken: sandbox.envdAccessToken,
430+
trafficAccessToken: sandbox.trafficAccessToken,
423431
envdVersion: sandbox.envdVersion,
424432
...config,
425433
}) 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
/**
@@ -552,6 +565,7 @@ export class SandboxApi {
552565
sandboxDomain: res.data!.domain || undefined,
553566
envdVersion: res.data!.envdVersion,
554567
envdAccessToken: res.data!.envdAccessToken,
568+
trafficAccessToken: res.data!.trafficAccessToken || undefined,
555569
}
556570
}
557571

@@ -590,6 +604,7 @@ export class SandboxApi {
590604
sandboxDomain: res.data!.domain || undefined,
591605
envdVersion: res.data!.envdVersion,
592606
envdAccessToken: res.data!.envdAccessToken,
607+
trafficAccessToken: res.data!.trafficAccessToken || undefined,
593608
}
594609
}
595610
}

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

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

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,13 +1,13 @@
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):
@@ -16,6 +16,7 @@ class SandboxOpts(TypedDict):
1616
envd_version: Version
1717
envd_access_token: Optional[str]
1818
sandbox_url: Optional[str]
19+
traffic_access_token: Optional[str]
1920
connection_config: ConnectionConfig
2021

2122

@@ -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 = self.connection_config.get_sandbox_url(
5053
self.sandbox_id, self.sandbox_domain
5154
)
@@ -72,6 +75,10 @@ def connection_config(self) -> ConnectionConfig:
7275
def _envd_version(self) -> Version:
7376
return self.__envd_version
7477

78+
@property
79+
def traffic_access_token(self) -> Optional[str]:
80+
return self.__traffic_access_token
81+
7582
@property
7683
def sandbox_domain(self) -> Optional[str]:
7784
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
@@ -678,6 +678,7 @@ async def _cls_connect(
678678
sandbox_domain=sandbox.domain,
679679
envd_version=Version(sandbox.envd_version),
680680
envd_access_token=envd_access_token,
681+
traffic_access_token=sandbox.traffic_access_token,
681682
connection_config=connection_config,
682683
)
683684

@@ -703,6 +704,7 @@ async def _create(
703704
sandbox_domain = None
704705
envd_version = ENVD_DEBUG_FALLBACK
705706
envd_access_token = None
707+
traffic_access_token = None
706708
else:
707709
response = await SandboxApi._create_sandbox(
708710
template=template or cls.default_template,
@@ -721,6 +723,7 @@ async def _create(
721723
sandbox_domain = response.sandbox_domain
722724
envd_version = Version(response.envd_version)
723725
envd_access_token = response.envd_access_token
726+
traffic_access_token = response.traffic_access_token
724727

725728
if envd_access_token is not None and not isinstance(
726729
envd_access_token, Unset
@@ -740,5 +743,6 @@ async def _create(
740743
sandbox_domain=sandbox_domain,
741744
envd_version=envd_version,
742745
envd_access_token=envd_access_token,
746+
traffic_access_token=traffic_access_token,
743747
connection_config=connection_config,
744748
)

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)