Skip to content

Commit ec5cf24

Browse files
authored
Merge pull request #62 from diggerhq/tls2
ALB-based TLS termination: proxy data-plane through control plane
2 parents 742be24 + 85913a7 commit ec5cf24

File tree

8 files changed

+410
-135
lines changed

8 files changed

+410
-135
lines changed

cmd/server/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ func main() {
137137
defer redisRegistry.Stop()
138138
opts.WorkerRegistry = redisRegistry
139139
log.Println("opensandbox: Redis worker registry started")
140+
141+
// Create sandbox API proxy for routing data-plane requests to workers
142+
if opts.Store != nil && opts.JWTIssuer != nil {
143+
opts.SandboxAPIProxy = proxy.NewSandboxAPIProxy(opts.Store, redisRegistry, opts.JWTIssuer)
144+
log.Println("opensandbox: sandbox API proxy enabled (data-plane requests proxied to workers)")
145+
}
140146
}
141147

142148
// Initialize EC2 compute pool + autoscaler (server mode with AWS configured)

deploy/ec2/Caddyfile

Lines changed: 0 additions & 10 deletions
This file was deleted.

deploy/ec2/caddy.service

Lines changed: 0 additions & 15 deletions
This file was deleted.

deploy/ec2/setup-instance.sh

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ set -euo pipefail
1111
# Prerequisites:
1212
# - Ubuntu 24.04 LTS AMI
1313
# - r7gd.metal (ARM64) or c7i.metal (x86_64) for production
14-
# - Security group: 443 (HTTPS), 8080 (HTTP), 9090 (gRPC), 9091 (metrics) open inbound
14+
# - Security group: 8080 (HTTP), 9090 (gRPC), 9091 (metrics) open from VPC only (workers are internal)
1515
# - SSH access
1616

1717
# Detect architecture
@@ -73,32 +73,6 @@ sudo apt-get install -y e2fsprogs
7373
echo "==> Installing Redis..."
7474
sudo apt-get install -y redis-server
7575

76-
# -------------------------------------------------------------------
77-
# Caddy (custom build with Route53 DNS module for wildcard certs)
78-
# -------------------------------------------------------------------
79-
echo "==> Installing Go (needed for xcaddy)..."
80-
GO_VERSION="1.23.6"
81-
curl -sL "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" | sudo tar -C /usr/local -xzf -
82-
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
83-
84-
echo "==> Building Caddy with Route53 DNS module..."
85-
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
86-
xcaddy build --with github.com/caddy-dns/route53 --output /tmp/caddy-custom
87-
sudo mv /tmp/caddy-custom /usr/local/bin/caddy
88-
sudo chmod +x /usr/local/bin/caddy
89-
90-
echo "==> Verifying Caddy has Route53 module..."
91-
caddy list-modules | grep route53 || { echo "ERROR: Caddy missing route53 module"; exit 1; }
92-
93-
echo "==> Installing Caddy config..."
94-
sudo mkdir -p /etc/caddy
95-
sudo cp /tmp/deploy-ec2/Caddyfile /etc/caddy/Caddyfile 2>/dev/null || \
96-
echo " NOTE: Copy deploy/ec2/Caddyfile to /etc/caddy/Caddyfile manually"
97-
98-
echo "==> Installing Caddy systemd unit..."
99-
sudo cp /tmp/deploy-ec2/caddy.service /etc/systemd/system/caddy.service 2>/dev/null || \
100-
echo " NOTE: Copy deploy/ec2/caddy.service to /etc/systemd/system/ manually"
101-
10276
# -------------------------------------------------------------------
10377
# NVMe instance storage (XFS with reflink for instant rootfs copies)
10478
# -------------------------------------------------------------------
@@ -233,7 +207,7 @@ WORKER_ID="w-use2-${SHORT_ID}"
233207
mkdir -p /etc/opensandbox
234208
cat > /etc/opensandbox/worker-identity.env << EOF
235209
OPENSANDBOX_WORKER_ID=${WORKER_ID}
236-
OPENSANDBOX_HTTP_ADDR=http://${PUBLIC_IP:-$PRIVATE_IP}:8080
210+
OPENSANDBOX_HTTP_ADDR=http://${PRIVATE_IP}:8080
237211
OPENSANDBOX_GRPC_ADVERTISE=${PRIVATE_IP}:9090
238212
EOF
239213
echo "opensandbox-identity: ${WORKER_ID} private=${PRIVATE_IP} public=${PUBLIC_IP:-none}"
@@ -305,14 +279,12 @@ sudo systemctl daemon-reload
305279
sudo systemctl enable opensandbox-nvme
306280
sudo systemctl enable opensandbox-identity
307281
sudo systemctl enable opensandbox-worker
308-
sudo systemctl enable caddy 2>/dev/null || true
309282

310283
# -------------------------------------------------------------------
311284
# Cleanup
312285
# -------------------------------------------------------------------
313-
echo "==> Cleaning up build tools..."
286+
echo "==> Cleaning up..."
314287
sudo apt-get clean
315-
sudo rm -rf /usr/local/go $HOME/go
316288

317289
echo ""
318290
echo "============================================"

internal/api/router.go

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type Server struct {
4646
ecrConfig *ecr.Config // nil if ECR not configured
4747
cfClient *cloudflare.Client // nil if Cloudflare not configured
4848
pendingCreates sync.Map // map[sandboxID]*pendingCreate — async sandbox creation tracking
49+
sandboxAPIProxy *proxy.SandboxAPIProxy // nil except in server mode (proxies data-plane to workers)
4950
}
5051

5152
// pendingCreate tracks an async sandbox creation.
@@ -73,6 +74,7 @@ type ServerOpts struct {
7374
CheckpointStore *storage.CheckpointStore // nil if hibernation not configured
7475
ECRConfig *ecr.Config // nil if ECR not configured
7576
CFClient *cloudflare.Client // nil if Cloudflare not configured
77+
SandboxAPIProxy *proxy.SandboxAPIProxy // nil except in server mode (proxies data-plane to workers)
7678
}
7779

7880
// NewServer creates a new API server with all routes configured.
@@ -102,6 +104,7 @@ func NewServer(mgr sandbox.Manager, ptyMgr *sandbox.PTYManager, apiKey string, o
102104
s.sandboxDomain = opts.SandboxDomain
103105
s.ecrConfig = opts.ECRConfig
104106
s.cfClient = opts.CFClient
107+
s.sandboxAPIProxy = opts.SandboxAPIProxy
105108
}
106109

107110
// Global middleware
@@ -132,7 +135,6 @@ func NewServer(mgr sandbox.Manager, ptyMgr *sandbox.PTYManager, apiKey string, o
132135
api.GET("/sandboxes", s.listSandboxes)
133136
api.GET("/sandboxes/:id", s.getSandbox)
134137
api.DELETE("/sandboxes/:id", s.killSandbox)
135-
api.POST("/sandboxes/:id/timeout", s.setTimeout)
136138

137139
// Hibernation
138140
api.POST("/sandboxes/:id/hibernate", s.hibernateSandbox)
@@ -155,32 +157,70 @@ func NewServer(mgr sandbox.Manager, ptyMgr *sandbox.PTYManager, apiKey string, o
155157
api.GET("/sandboxes/:id/preview", s.listPreviewURLs)
156158
api.DELETE("/sandboxes/:id/preview/:port", s.deletePreviewURL)
157159

158-
// Exec sessions (replaces old /commands)
159-
api.POST("/sandboxes/:id/exec", s.createExecSession)
160-
api.GET("/sandboxes/:id/exec", s.listExecSessions)
161-
api.GET("/sandboxes/:id/exec/:sessionID", s.execSessionWebSocket)
162-
api.POST("/sandboxes/:id/exec/:sessionID/kill", s.killExecSession)
163-
api.POST("/sandboxes/:id/exec/run", s.execRun)
164-
165-
// Agent sessions (Claude Agent SDK)
166-
api.POST("/sandboxes/:id/agent", s.createAgentSession)
167-
api.GET("/sandboxes/:id/agent", s.listAgentSessions)
168-
api.POST("/sandboxes/:id/agent/:sid/prompt", s.sendAgentPrompt)
169-
api.POST("/sandboxes/:id/agent/:sid/interrupt", s.interruptAgent)
170-
api.POST("/sandboxes/:id/agent/:sid/kill", s.killAgentSession)
171-
172-
// Filesystem
173-
api.GET("/sandboxes/:id/files", s.readFile)
174-
api.PUT("/sandboxes/:id/files", s.writeFile)
175-
api.GET("/sandboxes/:id/files/list", s.listDir)
176-
api.POST("/sandboxes/:id/files/mkdir", s.makeDir)
177-
api.DELETE("/sandboxes/:id/files", s.removeFile)
178-
179-
// PTY
180-
api.POST("/sandboxes/:id/pty", s.createPTY)
181-
api.GET("/sandboxes/:id/pty/:sessionID", s.ptyWebSocket)
182-
api.POST("/sandboxes/:id/pty/:sessionID/resize", s.resizePTY)
183-
api.DELETE("/sandboxes/:id/pty/:sessionID", s.killPTY)
160+
// Data-plane routes: in server mode, proxy to workers; otherwise handle locally
161+
if s.sandboxAPIProxy != nil {
162+
// Server mode: proxy all data-plane requests to the worker that owns the sandbox
163+
pxy := s.sandboxAPIProxy.ProxyHandler
164+
165+
// Exec
166+
api.POST("/sandboxes/:id/exec", pxy)
167+
api.GET("/sandboxes/:id/exec", pxy)
168+
api.GET("/sandboxes/:id/exec/:sessionID", pxy)
169+
api.POST("/sandboxes/:id/exec/:sessionID/kill", pxy)
170+
api.POST("/sandboxes/:id/exec/run", pxy)
171+
172+
// Agent
173+
api.POST("/sandboxes/:id/agent", pxy)
174+
api.GET("/sandboxes/:id/agent", pxy)
175+
api.POST("/sandboxes/:id/agent/:sid/prompt", pxy)
176+
api.POST("/sandboxes/:id/agent/:sid/interrupt", pxy)
177+
api.POST("/sandboxes/:id/agent/:sid/kill", pxy)
178+
179+
// Filesystem
180+
api.GET("/sandboxes/:id/files", pxy)
181+
api.PUT("/sandboxes/:id/files", pxy)
182+
api.GET("/sandboxes/:id/files/list", pxy)
183+
api.POST("/sandboxes/:id/files/mkdir", pxy)
184+
api.DELETE("/sandboxes/:id/files", pxy)
185+
186+
// PTY
187+
api.POST("/sandboxes/:id/pty", pxy)
188+
api.GET("/sandboxes/:id/pty/:sessionID", pxy)
189+
api.POST("/sandboxes/:id/pty/:sessionID/resize", pxy)
190+
api.DELETE("/sandboxes/:id/pty/:sessionID", pxy)
191+
192+
// Timeout
193+
api.POST("/sandboxes/:id/timeout", pxy)
194+
195+
// Token refresh
196+
api.POST("/sandboxes/:id/token/refresh", pxy)
197+
} else {
198+
// Combined/worker mode: handle locally
199+
api.POST("/sandboxes/:id/exec", s.createExecSession)
200+
api.GET("/sandboxes/:id/exec", s.listExecSessions)
201+
api.GET("/sandboxes/:id/exec/:sessionID", s.execSessionWebSocket)
202+
api.POST("/sandboxes/:id/exec/:sessionID/kill", s.killExecSession)
203+
api.POST("/sandboxes/:id/exec/run", s.execRun)
204+
205+
api.POST("/sandboxes/:id/agent", s.createAgentSession)
206+
api.GET("/sandboxes/:id/agent", s.listAgentSessions)
207+
api.POST("/sandboxes/:id/agent/:sid/prompt", s.sendAgentPrompt)
208+
api.POST("/sandboxes/:id/agent/:sid/interrupt", s.interruptAgent)
209+
api.POST("/sandboxes/:id/agent/:sid/kill", s.killAgentSession)
210+
211+
api.GET("/sandboxes/:id/files", s.readFile)
212+
api.PUT("/sandboxes/:id/files", s.writeFile)
213+
api.GET("/sandboxes/:id/files/list", s.listDir)
214+
api.POST("/sandboxes/:id/files/mkdir", s.makeDir)
215+
api.DELETE("/sandboxes/:id/files", s.removeFile)
216+
217+
api.POST("/sandboxes/:id/pty", s.createPTY)
218+
api.GET("/sandboxes/:id/pty/:sessionID", s.ptyWebSocket)
219+
api.POST("/sandboxes/:id/pty/:sessionID/resize", s.resizePTY)
220+
api.DELETE("/sandboxes/:id/pty/:sessionID", s.killPTY)
221+
222+
api.POST("/sandboxes/:id/timeout", s.setTimeout)
223+
}
184224

185225
// Snapshots (pre-built declarative images)
186226
api.POST("/snapshots", s.createSnapshot)

internal/api/sandbox.go

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,11 @@ func (s *Server) createSandboxRemote(c echo.Context, ctx context.Context, cfg ty
311311
}
312312

313313
resp := map[string]interface{}{
314-
"sandboxID": grpcResp.SandboxId,
315-
"connectURL": worker.HTTPAddr,
316-
"token": token,
317-
"status": grpcResp.Status,
318-
"region": region,
319-
"workerID": worker.ID,
314+
"sandboxID": grpcResp.SandboxId,
315+
"token": token,
316+
"status": grpcResp.Status,
317+
"region": region,
318+
"workerID": worker.ID,
320319
}
321320

322321
return c.JSON(http.StatusCreated, resp)
@@ -385,13 +384,6 @@ func (s *Server) getSandboxRemote(c echo.Context, sandboxID string) error {
385384
return c.JSON(http.StatusOK, resp)
386385
}
387386

388-
// Look up worker address
389-
worker := s.workerRegistry.GetWorker(session.WorkerID)
390-
connectURL := ""
391-
if worker != nil {
392-
connectURL = worker.HTTPAddr
393-
}
394-
395387
// Issue a fresh token
396388
var token string
397389
if s.jwtIssuer != nil {
@@ -402,14 +394,13 @@ func (s *Server) getSandboxRemote(c echo.Context, sandboxID string) error {
402394
}
403395

404396
resp := map[string]interface{}{
405-
"sandboxID": sandboxID,
406-
"connectURL": connectURL,
407-
"token": token,
408-
"status": session.Status,
409-
"region": session.Region,
410-
"workerID": session.WorkerID,
411-
"startedAt": session.StartedAt,
412-
"template": session.Template,
397+
"sandboxID": sandboxID,
398+
"token": token,
399+
"status": session.Status,
400+
"region": session.Region,
401+
"workerID": session.WorkerID,
402+
"startedAt": session.StartedAt,
403+
"template": session.Template,
413404
}
414405

415406
return c.JSON(http.StatusOK, resp)
@@ -544,12 +535,6 @@ func (s *Server) listSandboxesRemote(c echo.Context) error {
544535
"startedAt": sess.StartedAt,
545536
}
546537

547-
// Attach connectURL from registry
548-
worker := s.workerRegistry.GetWorker(sess.WorkerID)
549-
if worker != nil {
550-
entry["connectURL"] = worker.HTTPAddr
551-
}
552-
553538
// Issue fresh JWT
554539
if s.jwtIssuer != nil {
555540
token, err := s.jwtIssuer.IssueSandboxToken(orgID, sess.SandboxID, sess.WorkerID, 24*time.Hour)
@@ -565,13 +550,6 @@ func (s *Server) listSandboxesRemote(c echo.Context) error {
565550
}
566551

567552
func (s *Server) setTimeout(c echo.Context) error {
568-
// In server mode, timeout must be set directly on the worker via connectURL
569-
if s.workerRegistry != nil {
570-
return c.JSON(http.StatusBadRequest, map[string]string{
571-
"error": "timeout must be set directly on the worker via connectURL",
572-
})
573-
}
574-
575553
if s.router == nil {
576554
return c.JSON(http.StatusServiceUnavailable, errSandboxNotAvailable)
577555
}

0 commit comments

Comments
 (0)