Skip to content

Commit 96527f2

Browse files
authored
Replace Nginx based session proxy with orchestrator proxy (#417)
1 parent 5b57b96 commit 96527f2

File tree

16 files changed

+310
-21
lines changed

16 files changed

+310
-21
lines changed

main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ module "nomad" {
219219

220220
# Orchestrator
221221
orchestrator_port = var.orchestrator_port
222+
orchestrator_proxy_port = var.orchestrator_proxy_port
222223
fc_env_pipeline_bucket_name = module.buckets.fc_env_pipeline_bucket_name
223224

224225
# Template manager

packages/client-proxy/main.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ import (
2828
)
2929

3030
const (
31-
ServiceName = "client-proxy"
32-
dnsServer = "api.service.consul:5353"
33-
healthCheckPort = 3001
34-
port = 3002
35-
sandboxPort = 3003
36-
maxRetries = 3
31+
ServiceName = "client-proxy"
32+
dnsServer = "api.service.consul:5353"
33+
healthCheckPort = 3001
34+
port = 3002
35+
sandboxPort = 3003 // legacy session proxy port
36+
orchestratorProxyPort = 5007 // orchestrator proxy port
37+
maxRetries = 3
3738
)
3839

3940
var commitSHA string

packages/nomad/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ resource "nomad_job" "orchestrator" {
324324
jobspec = templatefile("${path.module}/orchestrator.hcl", {
325325
gcp_zone = var.gcp_zone
326326
port = var.orchestrator_port
327+
proxy_port = var.orchestrator_proxy_port
327328
environment = var.environment
328329
consul_acl_token = var.consul_acl_token_secret
329330

packages/nomad/orchestrator.hcl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ job "orchestrator" {
2525
}
2626
}
2727

28+
service {
29+
name = "orchestrator-proxy"
30+
port = "${proxy_port}"
31+
}
32+
2833
task "start" {
2934
driver = "raw_exec"
3035

@@ -45,7 +50,7 @@ job "orchestrator" {
4550

4651
config {
4752
command = "/bin/bash"
48-
args = ["-c", " chmod +x local/orchestrator && local/orchestrator --port ${port}"]
53+
args = ["-c", " chmod +x local/orchestrator && local/orchestrator --port ${port} --proxy-port ${proxy_port}"]
4954
}
5055

5156
artifact {

packages/nomad/variables.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ variable "orchestrator_port" {
185185
type = number
186186
}
187187

188+
variable "orchestrator_proxy_port" {
189+
type = number
190+
}
191+
188192
variable "fc_env_pipeline_bucket_name" {
189193
type = string
190194
}

packages/orchestrator/cmd/mock-sandbox/mock.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"go.opentelemetry.io/otel"
1414

1515
"github.com/e2b-dev/infra/packages/orchestrator/internal/dns"
16+
"github.com/e2b-dev/infra/packages/orchestrator/internal/proxy"
1617
"github.com/e2b-dev/infra/packages/orchestrator/internal/sandbox"
1718
"github.com/e2b-dev/infra/packages/orchestrator/internal/sandbox/nbd"
1819
"github.com/e2b-dev/infra/packages/orchestrator/internal/sandbox/network"
@@ -51,6 +52,8 @@ func main() {
5152
}()
5253

5354
dnsServer := dns.New()
55+
proxyServer := proxy.New(3333)
56+
5457
go func() {
5558
log.Printf("Starting DNS server")
5659

@@ -87,6 +90,7 @@ func main() {
8790
*buildId,
8891
*sandboxId+"-"+strconv.Itoa(v),
8992
dnsServer,
93+
proxyServer,
9094
time.Duration(*keepAlive)*time.Second,
9195
networkPool,
9296
templateCache,
@@ -104,6 +108,7 @@ func mockSandbox(
104108
buildId,
105109
sandboxId string,
106110
dns *dns.DNS,
111+
proxy *proxy.SandboxProxy,
107112
keepAlive time.Duration,
108113
networkPool *network.Pool,
109114
templateCache *template.Cache,
@@ -128,6 +133,7 @@ func mockSandbox(
128133
childCtx,
129134
tracer,
130135
dns,
136+
proxy,
131137
networkPool,
132138
templateCache,
133139
&orchestrator.SandboxConfig{

packages/orchestrator/cmd/mock-snapshot/mock.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"golang.org/x/sync/errgroup"
1515

1616
"github.com/e2b-dev/infra/packages/orchestrator/internal/dns"
17+
"github.com/e2b-dev/infra/packages/orchestrator/internal/proxy"
1718
"github.com/e2b-dev/infra/packages/orchestrator/internal/sandbox"
1819
"github.com/e2b-dev/infra/packages/orchestrator/internal/sandbox/nbd"
1920
"github.com/e2b-dev/infra/packages/orchestrator/internal/sandbox/network"
@@ -51,6 +52,7 @@ func main() {
5152
cancel()
5253
}()
5354

55+
proxyServer := proxy.New(3333)
5456
dnsServer := dns.New()
5557
go func() {
5658
log.Printf("Starting DNS server")
@@ -90,6 +92,7 @@ func main() {
9092
*buildId,
9193
*sandboxId+"-"+strconv.Itoa(v),
9294
dnsServer,
95+
proxyServer,
9396
time.Duration(*keepAlive)*time.Second,
9497
networkPool,
9598
templateCache,
@@ -113,6 +116,7 @@ func mockSnapshot(
113116
buildId,
114117
sandboxId string,
115118
dns *dns.DNS,
119+
proxy *proxy.SandboxProxy,
116120
keepAlive time.Duration,
117121
networkPool *network.Pool,
118122
templateCache *template.Cache,
@@ -137,6 +141,7 @@ func mockSnapshot(
137141
childCtx,
138142
tracer,
139143
dns,
144+
proxy,
140145
networkPool,
141146
templateCache,
142147
&orchestrator.SandboxConfig{
@@ -242,6 +247,7 @@ func mockSnapshot(
242247
childCtx,
243248
tracer,
244249
dns,
250+
proxy,
245251
networkPool,
246252
templateCache,
247253
&orchestrator.SandboxConfig{
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package proxy
2+
3+
import (
4+
"bytes"
5+
"context"
6+
_ "embed"
7+
"encoding/json"
8+
"fmt"
9+
"go.uber.org/zap"
10+
"html/template"
11+
"net/http"
12+
"net/http/httputil"
13+
"net/url"
14+
"regexp"
15+
"strconv"
16+
"strings"
17+
"time"
18+
19+
"github.com/e2b-dev/infra/packages/shared/pkg/meters"
20+
"github.com/e2b-dev/infra/packages/shared/pkg/smap"
21+
)
22+
23+
//go:embed proxy_browser_502.html
24+
var proxyBrowser502PageHtml string
25+
26+
var browserRegex = regexp.MustCompile(`(?i)mozilla|chrome|safari|firefox|edge|opera|msie`)
27+
var browserTemplate = template.Must(template.New("template").Parse(proxyBrowser502PageHtml))
28+
29+
type htmlTemplateData struct {
30+
SandboxId string
31+
SandboxHost string
32+
SandboxPort string
33+
}
34+
35+
type jsonTemplateData struct {
36+
Error string `json:"error"`
37+
SandboxId string `json:"sandboxId"`
38+
Port uint64 `json:"port"`
39+
}
40+
41+
type SandboxProxy struct {
42+
sandboxes *smap.Map[string]
43+
server *http.Server
44+
}
45+
46+
func New(port uint) *SandboxProxy {
47+
server := &http.Server{Addr: fmt.Sprintf(":%d", port)}
48+
49+
return &SandboxProxy{
50+
server: server,
51+
sandboxes: smap.New[string](),
52+
}
53+
}
54+
55+
func (p *SandboxProxy) AddSandbox(sandboxID, ip string) {
56+
p.sandboxes.Insert(sandboxID, ip)
57+
}
58+
59+
func (p *SandboxProxy) RemoveSandbox(sandboxID string, ip string) {
60+
p.sandboxes.RemoveCb(sandboxID, func(k string, v string, ok bool) bool { return ok && v == ip })
61+
}
62+
63+
func (p *SandboxProxy) Start() error {
64+
// similar values to our old the nginx configuration
65+
serverTransport := &http.Transport{
66+
Proxy: http.ProxyFromEnvironment,
67+
MaxIdleConns: 1024, // Matches worker_connections
68+
MaxIdleConnsPerHost: 8192, // Matches keepalive_requests
69+
IdleConnTimeout: 620 * time.Second, // Matches keepalive_timeout
70+
TLSHandshakeTimeout: 10 * time.Second, // Similar to client_header_timeout
71+
ResponseHeaderTimeout: 24 * time.Hour, // Matches proxy_read_timeout
72+
DisableKeepAlives: true, // Disable keep-alives, envd doesn't support idle connections
73+
}
74+
75+
p.server.Handler = http.HandlerFunc(p.proxyHandler(serverTransport))
76+
return p.server.ListenAndServe()
77+
}
78+
79+
func (p *SandboxProxy) Shutdown(ctx context.Context) {
80+
err := p.server.Shutdown(ctx)
81+
if err != nil {
82+
zap.L().Error("failed to shutdown proxy server", zap.Error(err))
83+
}
84+
}
85+
86+
func (p *SandboxProxy) proxyHandler(transport *http.Transport) func(w http.ResponseWriter, r *http.Request) {
87+
activeConnections, err := meters.GetUpDownCounter(meters.OrchestratorProxyActiveConnectionsCounterMeterName)
88+
if err != nil {
89+
zap.L().Error("failed to create active connections counter", zap.Error(err))
90+
}
91+
92+
return func(w http.ResponseWriter, r *http.Request) {
93+
if activeConnections != nil {
94+
activeConnections.Add(r.Context(), 1)
95+
defer func() {
96+
activeConnections.Add(r.Context(), -1)
97+
}()
98+
}
99+
100+
// Extract sandbox id from the host (<port>-<sandbox id>-<old client id>.e2b.dev)
101+
hostSplit := strings.Split(r.Host, "-")
102+
if len(hostSplit) < 2 {
103+
zap.L().Warn("invalid host to proxy", zap.String("host", r.Host))
104+
http.Error(w, "Invalid host", http.StatusBadRequest)
105+
return
106+
}
107+
108+
sandboxID := hostSplit[1]
109+
sandboxPortRaw := hostSplit[0]
110+
sandboxPort, sandboxPortErr := strconv.ParseUint(sandboxPortRaw, 10, 64)
111+
if sandboxPortErr != nil {
112+
zap.L().Warn("invalid sandbox port", zap.String("sandbox_port", sandboxPortRaw))
113+
http.Error(w, "Invalid sandbox port", http.StatusBadRequest)
114+
}
115+
116+
sbxIp, sbxFound := p.sandboxes.Get(sandboxID)
117+
if !sbxFound {
118+
zap.L().Warn("sandbox not found", zap.String("sandbox_id", sandboxID))
119+
http.Error(w, "Sandbox not found", http.StatusNotFound)
120+
return
121+
}
122+
123+
logger := zap.L().With(zap.String("sandbox_id", sandboxID), zap.String("sandbox_ip", sbxIp), zap.Uint64("sandbox_req_port", sandboxPort), zap.String("sandbox_port_path", r.URL.Path))
124+
125+
// We've resolved the node to proxy the request to
126+
logger.Debug("Proxying request")
127+
targetUrl := &url.URL{
128+
Scheme: "http",
129+
Host: fmt.Sprintf("%s:%d", sbxIp, sandboxPort),
130+
}
131+
132+
// Proxy the request
133+
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
134+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
135+
logger.Error("Reverse proxy error", zap.Error(err))
136+
137+
if p.isBrowser(r.UserAgent()) {
138+
res, resErr := p.buildHtmlClosedPortError(sandboxID, r.Host, sandboxPort)
139+
if resErr != nil {
140+
logger.Error("Failed to build HTML error response", zap.Error(resErr))
141+
w.WriteHeader(http.StatusInternalServerError)
142+
return
143+
}
144+
145+
w.WriteHeader(http.StatusBadGateway)
146+
w.Header().Add("Content-Type", "text/html")
147+
w.Write(res)
148+
return
149+
}
150+
151+
w.WriteHeader(http.StatusBadGateway)
152+
w.Header().Add("Content-Type", "application/json")
153+
w.Write(p.buildJsonClosedPortError(sandboxID, sandboxPort))
154+
}
155+
156+
proxy.ModifyResponse = func(resp *http.Response) error {
157+
if resp.StatusCode >= 500 {
158+
logger.Error("Backend responded with error", zap.Int("status_code", resp.StatusCode))
159+
} else {
160+
logger.Info("Backend responded", zap.Int("status_code", resp.StatusCode))
161+
}
162+
163+
return nil
164+
}
165+
166+
proxy.Transport = transport
167+
proxy.ServeHTTP(w, r)
168+
}
169+
}
170+
171+
func (p *SandboxProxy) buildHtmlClosedPortError(sandboxId string, host string, port uint64) ([]byte, error) {
172+
htmlResponse := new(bytes.Buffer)
173+
htmlVars := htmlTemplateData{SandboxId: sandboxId, SandboxHost: host, SandboxPort: strconv.FormatUint(port, 10)}
174+
175+
err := browserTemplate.Execute(htmlResponse, htmlVars)
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
return htmlResponse.Bytes(), nil
181+
}
182+
183+
func (p *SandboxProxy) buildJsonClosedPortError(sandboxId string, port uint64) []byte {
184+
response := jsonTemplateData{
185+
Error: "The sandbox is running but port is not open",
186+
SandboxId: sandboxId,
187+
Port: port,
188+
}
189+
190+
responseBytes, _ := json.Marshal(response)
191+
return responseBytes
192+
}
193+
194+
func (p *SandboxProxy) isBrowser(userAgent string) bool {
195+
return browserRegex.MatchString(userAgent)
196+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8">
4+
<meta name="viewport" content="width=device-width,initial-scale=1">
5+
<title>Closed Port Error</title>
6+
<style>:root{--brand:#ff8800;--error:#dc2626;--error-light:#fef2f2;--text:#1a1a1a;--background:#ffffff;--border:#e5e7eb;--details-bg:#f9fafb;--code-text:#374151;--muted-text:#6b7280}@media (prefers-color-scheme:dark){:root{--error:#ef4444;--error-light:#2a0f0f;--text:#e5e7eb;--background:#121212;--border:#2f2f2f;--details-bg:#1c1c1c;--code-text:#d1d5db;--muted-text:#9ca3af}}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f5f5f5;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem;color:var(--text)}@media (prefers-color-scheme:dark){body{background:#0a0a0a}}.error-card{background:var(--background);border-radius:12px;box-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);width:100%;max-width:600px;padding:1.5rem 2rem 2rem;position:relative}.logo{position:absolute;top:1rem;right:1.5rem;width:40px;height:40px;border-radius:50%;overflow:hidden}.error-header{margin-bottom:1.5rem;padding-right:3.5rem}.error-title{display:inline-block;color:var(--error);font-size:.9375rem;font-weight:500;margin-bottom:1rem;padding:.25rem .5rem;background:var(--error-light);border-radius:4px}.error-message{font-size:1.125rem;line-height:1.5;color:var(--error);font-weight:400;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}.error-details{background:var(--details-bg);border:1px solid var(--border);border-radius:8px;padding:1rem;margin-top:1.5rem}.error-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.875rem;color:var(--code-text)}.sandbox-url{color:var(--muted-text);font-size:.875rem;display:block;margin-bottom:.5rem}.highlight{font-weight:700}.help-text{margin-top:1.5rem;font-size:.875rem;color:var(--muted-text)}.debug-link{display:block;margin-top:2rem;color:var(--brand);text-decoration:none;font-size:.875rem}.debug-link:hover{text-decoration:underline}@media (max-width:640px){.error-card{margin:1rem;padding:1.25rem 1.5rem 1.5rem}.logo{top:.75rem;right:1rem;width:32px;height:32px}.error-header{padding-right:2.5rem}}</style>
7+
</head>
8+
<body>
9+
<main class="error-card">
10+
<img src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Symbol%20Gradient-Kr5pnWlK3ZhzBcRGf6Am4cNbJvY1Ge.svg" alt="Logo" class="logo">
11+
<div class="error-header">
12+
<h1 class="error-title">Closed Port Error</h1>
13+
<p class="error-message">The sandbox <span class="highlight" id="sandbox-id">{{.SandboxId}}</span> is running but there&#39s no service running on port <span class="highlight" id="port-number">{{.SandboxPort}}</span>.</p>
14+
</div>
15+
<div class="error-details">
16+
<span class="sandbox-url">{{.SandboxHost}}</span>
17+
<div class="error-code">Connection refused on port <span class="highlight" id="port-number-code">{{.SandboxPort}}</span></div>
18+
</div>
19+
<p class="help-text">Please ensure that your service is properly configured and running on the specified port.</p>
20+
<a class="debug-link" href="https://e2b.dev/docs/sdk-reference/cli/v1.0.9/sandbox#e2b-sandbox-logs">Check the sandbox logs for more information →</a>
21+
</main>
22+
</body>
23+
</html>

0 commit comments

Comments
 (0)