Skip to content

Commit b23fc76

Browse files
committed
feat: add host mask and public access auth
1 parent addcadd commit b23fc76

File tree

14 files changed

+438
-10
lines changed

14 files changed

+438
-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: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import os
2-
from types import TracebackType
31
import json
42
import logging
5-
from typing import Optional, Union
6-
from httpx import Limits, BaseTransport, AsyncBaseTransport
3+
import os
74
from dataclasses import dataclass
5+
from types import TracebackType
6+
from typing import Optional, Union
7+
8+
from httpx import AsyncBaseTransport, BaseTransport, Limits
89

910
from e2b.api.client.client import AuthenticatedClient
10-
from e2b.connection_config import ConnectionConfig
11+
from e2b.api.client.types import Response
1112
from e2b.api.metadata import default_headers
13+
from e2b.connection_config import ConnectionConfig
1214
from e2b.exceptions import (
1315
AuthenticationException,
14-
SandboxException,
1516
RateLimitException,
17+
SandboxException,
1618
)
17-
from e2b.api.client.types import Response
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -31,6 +32,7 @@ class SandboxCreateResponse:
3132
sandbox_domain: Optional[str]
3233
envd_version: str
3334
envd_access_token: str
35+
traffic_access_token: Optional[str]
3436

3537

3638
def handle_api_exception(

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
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 packaging.version import Version
5+
76
from e2b.connection_config import ConnectionConfig, default_username
87
from e2b.envd.api import ENVD_API_FILES_ROUTE
98
from e2b.envd.versions import ENVD_DEFAULT_USER
9+
from e2b.sandbox.signature import get_signature
1010

1111

1212
class SandboxOpts(TypedDict):
@@ -15,6 +15,7 @@ class SandboxOpts(TypedDict):
1515
envd_version: Version
1616
envd_access_token: Optional[str]
1717
sandbox_url: Optional[str]
18+
traffic_access_token: Optional[str]
1819
connection_config: ConnectionConfig
1920

2021

@@ -33,12 +34,14 @@ def __init__(
3334
envd_access_token: Optional[str],
3435
sandbox_domain: Optional[str],
3536
connection_config: ConnectionConfig,
37+
traffic_access_token: Optional[str] = None,
3638
):
3739
self.__connection_config = connection_config
3840
self.__sandbox_id = sandbox_id
3941
self.__sandbox_domain = sandbox_domain or self.connection_config.domain
4042
self.__envd_version = envd_version
4143
self.__envd_access_token = envd_access_token
44+
self.__traffic_access_token = traffic_access_token
4245
self.__envd_api_url = self.connection_config.get_sandbox_url(
4346
self.sandbox_id, self.sandbox_domain
4447
)
@@ -65,6 +68,10 @@ def connection_config(self) -> ConnectionConfig:
6568
def _envd_version(self) -> Version:
6669
return self.__envd_version
6770

71+
@property
72+
def traffic_access_token(self) -> Optional[str]:
73+
return self.__traffic_access_token
74+
6875
@property
6976
def sandbox_domain(self) -> Optional[str]:
7077
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
@@ -664,6 +664,7 @@ async def _cls_connect(
664664
sandbox_domain=sandbox.domain,
665665
envd_version=Version(sandbox.envd_version),
666666
envd_access_token=envd_access_token,
667+
traffic_access_token=sandbox.traffic_access_token,
667668
connection_config=connection_config,
668669
)
669670

@@ -689,6 +690,7 @@ async def _create(
689690
sandbox_domain = None
690691
envd_version = ENVD_DEBUG_FALLBACK
691692
envd_access_token = None
693+
traffic_access_token = None
692694
else:
693695
response = await SandboxApi._create_sandbox(
694696
template=template or cls.default_template,
@@ -707,6 +709,7 @@ async def _create(
707709
sandbox_domain = response.sandbox_domain
708710
envd_version = Version(response.envd_version)
709711
envd_access_token = response.envd_access_token
712+
traffic_access_token = response.traffic_access_token
710713

711714
if envd_access_token is not None and not isinstance(
712715
envd_access_token, Unset
@@ -726,5 +729,6 @@ async def _create(
726729
sandbox_domain=sandbox_domain,
727730
envd_version=envd_version,
728731
envd_access_token=envd_access_token,
732+
traffic_access_token=traffic_access_token,
729733
connection_config=connection_config,
730734
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ async def _create_sandbox(
200200
sandbox_domain=res.parsed.domain,
201201
envd_version=res.parsed.envd_version,
202202
envd_access_token=res.parsed.envd_access_token,
203+
traffic_access_token=res.parsed.traffic_access_token,
203204
)
204205

205206
@classmethod

0 commit comments

Comments
 (0)