-
Notifications
You must be signed in to change notification settings - Fork 214
Replace Nginx based session proxy with orchestrator proxy #417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+310
−21
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
094aa82
client proxy is routing traffic to orchestrator proxy instead of sess…
sitole 42baa91
added proxy port for orchestrator
sitole 4fb1da7
orchestrator session http proxy
sitole 670c9fc
port number for orchestrator proxy in nomad
sitole a8cf105
usage of session proxy inside orchestrator
sitole bae57a1
sorted imports, missing proxy arg in test mocks
sitole fed98cc
renamed var for orchestrator proxy port
sitole 8ab9969
orchestrator: renamed session proxy to sandbox proxy
sitole 1e3060c
simplified proxy error handling via channel
sitole 9e0a8f3
revert traffic routing to sandbox proxy
sitole ab5f432
use html template for replacing variables
sitole 13097aa
proxy sandbox removal with strictly exact ip
sitole edddf82
fixed typo
sitole 3c22503
initialize html temp once, use type structs for variables
sitole 28ab252
use regexp for browser user-agent matching
sitole d083756
orchestrator proxy service shutdown without timeout specified
sitole bb505d3
separated metric name for orchestrator proxy
sitole 97245dc
propagate upstream error into log
sitole 5ebfce5
go template can work only with sturcts with visible fields
sitole 6021052
fixed json serializaiton after moved from anonymous struct to named one
sitole de1ccdd
disable keep alives for orchestrator proxy to match settings of curre…
sitole aed10d9
fixed wrong name for sandbox id in err response
sitole File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| package proxy | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| _ "embed" | ||
| "encoding/json" | ||
| "fmt" | ||
| "go.uber.org/zap" | ||
| "html/template" | ||
| "net/http" | ||
| "net/http/httputil" | ||
| "net/url" | ||
| "regexp" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/e2b-dev/infra/packages/shared/pkg/meters" | ||
| "github.com/e2b-dev/infra/packages/shared/pkg/smap" | ||
| ) | ||
|
|
||
| //go:embed proxy_browser_502.html | ||
| var proxyBrowser502PageHtml string | ||
|
|
||
| var browserRegex = regexp.MustCompile(`(?i)mozilla|chrome|safari|firefox|edge|opera|msie`) | ||
| var browserTemplate = template.Must(template.New("template").Parse(proxyBrowser502PageHtml)) | ||
|
|
||
| type htmlTemplateData struct { | ||
| SandboxId string | ||
| SandboxHost string | ||
| SandboxPort string | ||
| } | ||
|
|
||
| type jsonTemplateData struct { | ||
| Error string `json:"error"` | ||
| SandboxId string `json:"sandboxId"` | ||
| Port uint64 `json:"port"` | ||
| } | ||
|
|
||
| type SandboxProxy struct { | ||
| sandboxes *smap.Map[string] | ||
| server *http.Server | ||
| } | ||
|
|
||
| func New(port uint) *SandboxProxy { | ||
| server := &http.Server{Addr: fmt.Sprintf(":%d", port)} | ||
|
|
||
| return &SandboxProxy{ | ||
| server: server, | ||
| sandboxes: smap.New[string](), | ||
| } | ||
| } | ||
|
|
||
| func (p *SandboxProxy) AddSandbox(sandboxID, ip string) { | ||
| p.sandboxes.Insert(sandboxID, ip) | ||
| } | ||
|
|
||
| func (p *SandboxProxy) RemoveSandbox(sandboxID string, ip string) { | ||
| p.sandboxes.RemoveCb(sandboxID, func(k string, v string, ok bool) bool { return ok && v == ip }) | ||
| } | ||
|
|
||
| func (p *SandboxProxy) Start() error { | ||
| // similar values to our old the nginx configuration | ||
| serverTransport := &http.Transport{ | ||
| Proxy: http.ProxyFromEnvironment, | ||
| MaxIdleConns: 1024, // Matches worker_connections | ||
| MaxIdleConnsPerHost: 8192, // Matches keepalive_requests | ||
| IdleConnTimeout: 620 * time.Second, // Matches keepalive_timeout | ||
| TLSHandshakeTimeout: 10 * time.Second, // Similar to client_header_timeout | ||
| ResponseHeaderTimeout: 24 * time.Hour, // Matches proxy_read_timeout | ||
| DisableKeepAlives: true, // Disable keep-alives, envd doesn't support idle connections | ||
| } | ||
|
|
||
| p.server.Handler = http.HandlerFunc(p.proxyHandler(serverTransport)) | ||
| return p.server.ListenAndServe() | ||
| } | ||
|
|
||
| func (p *SandboxProxy) Shutdown(ctx context.Context) { | ||
| err := p.server.Shutdown(ctx) | ||
| if err != nil { | ||
| zap.L().Error("failed to shutdown proxy server", zap.Error(err)) | ||
| } | ||
| } | ||
|
|
||
| func (p *SandboxProxy) proxyHandler(transport *http.Transport) func(w http.ResponseWriter, r *http.Request) { | ||
| activeConnections, err := meters.GetUpDownCounter(meters.OrchestratorProxyActiveConnectionsCounterMeterName) | ||
| if err != nil { | ||
| zap.L().Error("failed to create active connections counter", zap.Error(err)) | ||
| } | ||
|
|
||
| return func(w http.ResponseWriter, r *http.Request) { | ||
| if activeConnections != nil { | ||
| activeConnections.Add(r.Context(), 1) | ||
| defer func() { | ||
| activeConnections.Add(r.Context(), -1) | ||
| }() | ||
| } | ||
|
|
||
| // Extract sandbox id from the host (<port>-<sandbox id>-<old client id>.e2b.dev) | ||
| hostSplit := strings.Split(r.Host, "-") | ||
| if len(hostSplit) < 2 { | ||
| zap.L().Warn("invalid host to proxy", zap.String("host", r.Host)) | ||
| http.Error(w, "Invalid host", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| sandboxID := hostSplit[1] | ||
| sandboxPortRaw := hostSplit[0] | ||
| sandboxPort, sandboxPortErr := strconv.ParseUint(sandboxPortRaw, 10, 64) | ||
| if sandboxPortErr != nil { | ||
| zap.L().Warn("invalid sandbox port", zap.String("sandbox_port", sandboxPortRaw)) | ||
| http.Error(w, "Invalid sandbox port", http.StatusBadRequest) | ||
| } | ||
|
|
||
| sbxIp, sbxFound := p.sandboxes.Get(sandboxID) | ||
| if !sbxFound { | ||
| zap.L().Warn("sandbox not found", zap.String("sandbox_id", sandboxID)) | ||
| http.Error(w, "Sandbox not found", http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
||
| 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)) | ||
|
|
||
| // We've resolved the node to proxy the request to | ||
| logger.Debug("Proxying request") | ||
| targetUrl := &url.URL{ | ||
| Scheme: "http", | ||
| Host: fmt.Sprintf("%s:%d", sbxIp, sandboxPort), | ||
| } | ||
|
|
||
| // Proxy the request | ||
| proxy := httputil.NewSingleHostReverseProxy(targetUrl) | ||
| proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | ||
| logger.Error("Reverse proxy error", zap.Error(err)) | ||
|
|
||
| if p.isBrowser(r.UserAgent()) { | ||
| res, resErr := p.buildHtmlClosedPortError(sandboxID, r.Host, sandboxPort) | ||
| if resErr != nil { | ||
| logger.Error("Failed to build HTML error response", zap.Error(resErr)) | ||
| w.WriteHeader(http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| w.WriteHeader(http.StatusBadGateway) | ||
| w.Header().Add("Content-Type", "text/html") | ||
| w.Write(res) | ||
| return | ||
| } | ||
|
|
||
| w.WriteHeader(http.StatusBadGateway) | ||
| w.Header().Add("Content-Type", "application/json") | ||
| w.Write(p.buildJsonClosedPortError(sandboxID, sandboxPort)) | ||
| } | ||
|
|
||
| proxy.ModifyResponse = func(resp *http.Response) error { | ||
| if resp.StatusCode >= 500 { | ||
| logger.Error("Backend responded with error", zap.Int("status_code", resp.StatusCode)) | ||
| } else { | ||
| logger.Info("Backend responded", zap.Int("status_code", resp.StatusCode)) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| proxy.Transport = transport | ||
| proxy.ServeHTTP(w, r) | ||
| } | ||
| } | ||
|
|
||
| func (p *SandboxProxy) buildHtmlClosedPortError(sandboxId string, host string, port uint64) ([]byte, error) { | ||
| htmlResponse := new(bytes.Buffer) | ||
| htmlVars := htmlTemplateData{SandboxId: sandboxId, SandboxHost: host, SandboxPort: strconv.FormatUint(port, 10)} | ||
|
|
||
| err := browserTemplate.Execute(htmlResponse, htmlVars) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return htmlResponse.Bytes(), nil | ||
| } | ||
|
|
||
| func (p *SandboxProxy) buildJsonClosedPortError(sandboxId string, port uint64) []byte { | ||
| response := jsonTemplateData{ | ||
| Error: "The sandbox is running but port is not open", | ||
| SandboxId: sandboxId, | ||
| Port: port, | ||
| } | ||
|
|
||
| responseBytes, _ := json.Marshal(response) | ||
| return responseBytes | ||
| } | ||
|
|
||
| func (p *SandboxProxy) isBrowser(userAgent string) bool { | ||
| return browserRegex.MatchString(userAgent) | ||
| } | ||
23 changes: 23 additions & 0 deletions
23
packages/orchestrator/internal/proxy/proxy_browser_502.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
| <title>Closed Port Error</title> | ||
| <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> | ||
| </head> | ||
| <body> | ||
| <main class="error-card"> | ||
| <img src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Symbol%20Gradient-Kr5pnWlK3ZhzBcRGf6Am4cNbJvY1Ge.svg" alt="Logo" class="logo"> | ||
| <div class="error-header"> | ||
| <h1 class="error-title">Closed Port Error</h1> | ||
| <p class="error-message">The sandbox <span class="highlight" id="sandbox-id">{{.SandboxId}}</span> is running but there's no service running on port <span class="highlight" id="port-number">{{.SandboxPort}}</span>.</p> | ||
| </div> | ||
| <div class="error-details"> | ||
| <span class="sandbox-url">{{.SandboxHost}}</span> | ||
| <div class="error-code">Connection refused on port <span class="highlight" id="port-number-code">{{.SandboxPort}}</span></div> | ||
| </div> | ||
| <p class="help-text">Please ensure that your service is properly configured and running on the specified port.</p> | ||
| <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> | ||
| </main> | ||
| </body> | ||
| </html> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.