feat: add upstream proxy support to aiproxy for passthrough requests (#21512)

## Description

Adds upstream proxy support for AI Bridge Proxy passthrough requests.
This allows aiproxy to forward non-allowlisted requests through an
upstream proxy. Currently, the only supported configuration is when
aiproxy is the first proxy in the chain (client → aiproxy → upstream
proxy).

## Changes

* Add `--aibridge-proxy-upstream` option to configure an upstream
HTTP/HTTPS proxy URL for passthrough requests
* Add `--aibridge-proxy-upstream-ca` option to trust custom CA
certificates for HTTPS upstream proxies
* Passthrough requests (non-allowlisted domains) are forwarded through
the upstream proxy
* MITM'd requests (allowlisted domains) continue to go directly to
aibridge, not through the upstream proxy
* Add tests for upstream proxy configuration and request routing

Closes: https://github.com/coder/internal/issues/1204
This commit is contained in:
Susana Ferreira
2026-01-19 08:50:57 +00:00
committed by GitHub
parent 1813605012
commit a406ed7cc5
13 changed files with 520 additions and 13 deletions
+11
View File
@@ -165,6 +165,17 @@ AI BRIDGE PROXY OPTIONS:
--aibridge-proxy-listen-addr string, $CODER_AIBRIDGE_PROXY_LISTEN_ADDR (default: :8888)
The address the AI Bridge Proxy will listen on.
--aibridge-proxy-upstream string, $CODER_AIBRIDGE_PROXY_UPSTREAM
URL of an upstream HTTP proxy to chain passthrough (non-allowlisted)
requests through. Format: http://[user:pass@]host:port or
https://[user:pass@]host:port.
--aibridge-proxy-upstream-ca string, $CODER_AIBRIDGE_PROXY_UPSTREAM_CA
Path to a PEM-encoded CA certificate to trust for the upstream proxy's
TLS connection. Only needed for HTTPS upstream proxies with
certificates not trusted by the system. If not provided, the system
certificate pool is used.
CLIENT OPTIONS:
These options change the behavior of how clients interact with the Coder.
Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI.
+9
View File
@@ -798,6 +798,15 @@ aibridgeproxy:
domain_allowlist:
- api.anthropic.com
- api.openai.com
# URL of an upstream HTTP proxy to chain passthrough (non-allowlisted) requests
# through. Format: http://[user:pass@]host:port or https://[user:pass@]host:port.
# (default: <unset>, type: string)
upstream_proxy: ""
# Path to a PEM-encoded CA certificate to trust for the upstream proxy's TLS
# connection. Only needed for HTTPS upstream proxies with certificates not trusted
# by the system. If not provided, the system certificate pool is used.
# (default: <unset>, type: string)
upstream_proxy_ca: ""
# Configure data retention policies for various database tables. Retention
# policies automatically purge old data to reduce database size and improve
# performance. Setting a retention duration to 0 disables automatic purging for
+6
View File
@@ -12157,6 +12157,12 @@ const docTemplate = `{
},
"listen_addr": {
"type": "string"
},
"upstream_proxy": {
"type": "string"
},
"upstream_proxy_ca": {
"type": "string"
}
}
},
+6
View File
@@ -10809,6 +10809,12 @@
},
"listen_addr": {
"type": "string"
},
"upstream_proxy": {
"type": "string"
},
"upstream_proxy_ca": {
"type": "string"
}
}
},
+22
View File
@@ -3547,6 +3547,26 @@ Write out the current server config as YAML to stdout.`,
Group: &deploymentGroupAIBridgeProxy,
YAML: "domain_allowlist",
},
{
Name: "AI Bridge Proxy Upstream Proxy",
Description: "URL of an upstream HTTP proxy to chain passthrough (non-allowlisted) requests through. Format: http://[user:pass@]host:port or https://[user:pass@]host:port.",
Flag: "aibridge-proxy-upstream",
Env: "CODER_AIBRIDGE_PROXY_UPSTREAM",
Value: &c.AI.BridgeProxyConfig.UpstreamProxy,
Default: "",
Group: &deploymentGroupAIBridgeProxy,
YAML: "upstream_proxy",
},
{
Name: "AI Bridge Proxy Upstream Proxy CA",
Description: "Path to a PEM-encoded CA certificate to trust for the upstream proxy's TLS connection. Only needed for HTTPS upstream proxies with certificates not trusted by the system. If not provided, the system certificate pool is used.",
Flag: "aibridge-proxy-upstream-ca",
Env: "CODER_AIBRIDGE_PROXY_UPSTREAM_CA",
Value: &c.AI.BridgeProxyConfig.UpstreamProxyCA,
Default: "",
Group: &deploymentGroupAIBridgeProxy,
YAML: "upstream_proxy_ca",
},
// Retention settings
{
@@ -3647,6 +3667,8 @@ type AIBridgeProxyConfig struct {
CertFile serpent.String `json:"cert_file" typescript:",notnull"`
KeyFile serpent.String `json:"key_file" typescript:",notnull"`
DomainAllowlist serpent.StringArray `json:"domain_allowlist" typescript:",notnull"`
UpstreamProxy serpent.String `json:"upstream_proxy" typescript:",notnull"`
UpstreamProxyCA serpent.String `json:"upstream_proxy_ca" typescript:",notnull"`
}
type AIConfig struct {
+3 -1
View File
@@ -169,7 +169,9 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
],
"enabled": true,
"key_file": "string",
"listen_addr": "string"
"listen_addr": "string",
"upstream_proxy": "string",
"upstream_proxy_ca": "string"
},
"bridge": {
"anthropic": {
+21 -11
View File
@@ -604,19 +604,23 @@
],
"enabled": true,
"key_file": "string",
"listen_addr": "string"
"listen_addr": "string",
"upstream_proxy": "string",
"upstream_proxy_ca": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|-----------------|----------|--------------|-------------|
| `cert_file` | string | false | | |
| `domain_allowlist` | array of string | false | | |
| `enabled` | boolean | false | | |
| `key_file` | string | false | | |
| `listen_addr` | string | false | | |
| Name | Type | Required | Restrictions | Description |
|---------------------|-----------------|----------|--------------|-------------|
| `cert_file` | string | false | | |
| `domain_allowlist` | array of string | false | | |
| `enabled` | boolean | false | | |
| `key_file` | string | false | | |
| `listen_addr` | string | false | | |
| `upstream_proxy` | string | false | | |
| `upstream_proxy_ca` | string | false | | |
## codersdk.AIBridgeTokenUsage
@@ -723,7 +727,9 @@
],
"enabled": true,
"key_file": "string",
"listen_addr": "string"
"listen_addr": "string",
"upstream_proxy": "string",
"upstream_proxy_ca": "string"
},
"bridge": {
"anthropic": {
@@ -2639,7 +2645,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
],
"enabled": true,
"key_file": "string",
"listen_addr": "string"
"listen_addr": "string",
"upstream_proxy": "string",
"upstream_proxy_ca": "string"
},
"bridge": {
"anthropic": {
@@ -3184,7 +3192,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
],
"enabled": true,
"key_file": "string",
"listen_addr": "string"
"listen_addr": "string",
"upstream_proxy": "string",
"upstream_proxy_ca": "string"
},
"bridge": {
"anthropic": {
+20
View File
@@ -1889,6 +1889,26 @@ Path to the CA certificate file for AI Bridge Proxy.
Path to the CA private key file for AI Bridge Proxy.
### --aibridge-proxy-upstream
| | |
|-------------|---------------------------------------------|
| Type | <code>string</code> |
| Environment | <code>$CODER_AIBRIDGE_PROXY_UPSTREAM</code> |
| YAML | <code>aibridgeproxy.upstream_proxy</code> |
URL of an upstream HTTP proxy to chain passthrough (non-allowlisted) requests through. Format: http://[user:pass@]host:port or https://[user:pass@]host:port.
### --aibridge-proxy-upstream-ca
| | |
|-------------|------------------------------------------------|
| Type | <code>string</code> |
| Environment | <code>$CODER_AIBRIDGE_PROXY_UPSTREAM_CA</code> |
| YAML | <code>aibridgeproxy.upstream_proxy_ca</code> |
Path to a PEM-encoded CA certificate to trust for the upstream proxy's TLS connection. Only needed for HTTPS upstream proxies with certificates not trusted by the system. If not provided, the system certificate pool is used.
### --audit-logs-retention
| | |
@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"slices"
"strings"
"sync"
@@ -74,6 +75,16 @@ type Options struct {
// Only requests to these domains will be MITM'd and forwarded to aibridged.
// Requests to other domains will be tunneled directly without decryption.
DomainAllowlist []string
// UpstreamProxy is the URL of an upstream HTTP proxy to chain passthrough
// (non-allowlisted) requests through. If empty, passthrough requests connect
// directly to their destinations.
// Format: http://[user:pass@]host:port or https://[user:pass@]host:port
UpstreamProxy string
// UpstreamProxyCA is the path to a PEM-encoded CA certificate file to trust
// for the upstream proxy's TLS connection. Only needed for HTTPS upstream
// proxies with certificates not trusted by the system. If empty, the system
// certificate pool is used.
UpstreamProxyCA string
}
func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error) {
@@ -132,6 +143,65 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error)
proxy.CertStore = NewCertCache()
}
// Always set secure TLS defaults, overriding goproxy's default.
// This ensures secure TLS connections for:
// - HTTPS upstream proxy connections
// - MITM'd requests if aibridge uses HTTPS
rootCAs, err := x509.SystemCertPool()
if err != nil {
return nil, xerrors.Errorf("failed to load system certificate pool: %w", err)
}
// Configure upstream proxy for passthrough (non-allowlisted) requests.
// This only affects CONNECT requests to domains not in the allowlist.
// MITM'd requests (allowlisted domains) are handled by aiproxy and forwarded
// to aibridge directly, not through the upstream proxy. AI Bridge respects
// proxy environment variables if set, so the upstream proxy is used at that
// layer instead.
if opts.UpstreamProxy != "" {
upstreamURL, err := url.Parse(opts.UpstreamProxy)
if err != nil {
return nil, xerrors.Errorf("invalid upstream proxy URL %q: %w", opts.UpstreamProxy, err)
}
logger.Info(ctx, "configuring upstream proxy for passthrough requests",
slog.F("upstream", upstreamURL.Host),
)
// Set transport without Proxy to ensure MITM'd requests go directly to aibridge,
// not through any upstream proxy.
proxy.Tr = &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: rootCAs,
},
}
// Add custom CA certificate if provided (for corporate proxies with private CAs).
// If no CA certificate is provided, the system certificate pool is used.
if opts.UpstreamProxyCA != "" {
if upstreamURL.Scheme == "https" {
caCert, err := os.ReadFile(opts.UpstreamProxyCA)
if err != nil {
return nil, xerrors.Errorf("failed to read upstream proxy CA certificate from %q: %w", opts.UpstreamProxyCA, err)
}
if !rootCAs.AppendCertsFromPEM(caCert) {
return nil, xerrors.Errorf("failed to parse upstream proxy CA certificate")
}
logger.Info(ctx, "configured upstream proxy CA certificate")
} else {
logger.Warn(ctx, "upstream proxy CA certificate is only used for HTTPS upstream proxies, ignoring",
slog.F("upstream_scheme", upstreamURL.Scheme),
)
}
}
// Configure passthrough CONNECT requests to go through upstream proxy.
// This only affects non-allowlisted domains; allowlisted domains are
// MITM'd and forwarded to aibridge.
proxy.ConnectDial = proxy.NewConnectDialToProxy(opts.UpstreamProxy)
}
srv := &Server{
ctx: ctx,
logger: logger,
@@ -8,6 +8,7 @@ import (
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"math/big"
"net"
@@ -101,6 +102,8 @@ type testProxyConfig struct {
allowedPorts []string
certStore *aibridgeproxyd.CertCache
domainAllowlist []string
upstreamProxy string
upstreamProxyCA string
}
type testProxyOption func(*testProxyConfig)
@@ -129,6 +132,18 @@ func withDomainAllowlist(domains ...string) testProxyOption {
}
}
func withUpstreamProxy(upstreamProxy string) testProxyOption {
return func(cfg *testProxyConfig) {
cfg.upstreamProxy = upstreamProxy
}
}
func withUpstreamProxyCA(upstreamProxyCA string) testProxyOption {
return func(cfg *testProxyConfig) {
cfg.upstreamProxyCA = upstreamProxyCA
}
}
// newTestProxy creates a new AI Bridge Proxy server for testing.
// It uses the shared test CA and registers cleanup automatically.
// It waits for the proxy server to be ready before returning.
@@ -154,6 +169,8 @@ func newTestProxy(t *testing.T, opts ...testProxyOption) *aibridgeproxyd.Server
KeyFile: keyFile,
AllowedPorts: cfg.allowedPorts,
DomainAllowlist: cfg.domainAllowlist,
UpstreamProxy: cfg.upstreamProxy,
UpstreamProxyCA: cfg.upstreamProxyCA,
}
if cfg.certStore != nil {
aibridgeOpts.CertStore = cfg.certStore
@@ -446,6 +463,43 @@ func TestNew(t *testing.T) {
require.Contains(t, err.Error(), "invalid port in domain")
})
t.Run("InvalidUpstreamProxy", func(t *testing.T) {
t.Parallel()
certFile, keyFile := getSharedTestCA(t)
logger := slogtest.Make(t, nil)
_, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{
ListenAddr: "127.0.0.1:0",
CoderAccessURL: "http://localhost:3000",
CertFile: certFile,
KeyFile: keyFile,
DomainAllowlist: []string{"api.anthropic.com"},
UpstreamProxy: "://invalid-url",
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid upstream proxy URL")
})
t.Run("UpstreamProxyCAFileNotFound", func(t *testing.T) {
t.Parallel()
certFile, keyFile := getSharedTestCA(t)
logger := slogtest.Make(t, nil)
_, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{
ListenAddr: "127.0.0.1:0",
CoderAccessURL: "http://localhost:3000",
CertFile: certFile,
KeyFile: keyFile,
DomainAllowlist: []string{"api.anthropic.com"},
UpstreamProxy: "https://proxy.example.com:8080",
UpstreamProxyCA: "/nonexistent/ca.pem",
})
require.Error(t, err)
require.Contains(t, err.Error(), "failed to read upstream proxy CA certificate")
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
@@ -461,9 +515,44 @@ func TestNew(t *testing.T) {
})
require.NoError(t, err)
require.NotNil(t, srv)
})
err = srv.Close()
t.Run("SuccessWithUpstreamProxy", func(t *testing.T) {
t.Parallel()
certFile, keyFile := getSharedTestCA(t)
logger := slogtest.Make(t, nil)
srv, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{
ListenAddr: "127.0.0.1:0",
CoderAccessURL: "http://localhost:3000",
CertFile: certFile,
KeyFile: keyFile,
DomainAllowlist: []string{"api.anthropic.com", "api.openai.com"},
UpstreamProxy: "http://proxy.example.com:8080",
})
require.NoError(t, err)
require.NotNil(t, srv)
})
t.Run("SuccessWithHTTPSUpstreamProxyAndCA", func(t *testing.T) {
t.Parallel()
certFile, keyFile := getSharedTestCA(t)
logger := slogtest.Make(t, nil)
// Use the shared test CA as the upstream proxy CA (it's a valid PEM cert)
srv, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{
ListenAddr: "127.0.0.1:0",
CoderAccessURL: "http://localhost:3000",
CertFile: certFile,
KeyFile: keyFile,
DomainAllowlist: []string{"api.anthropic.com"},
UpstreamProxy: "https://proxy.example.com:8080",
UpstreamProxyCA: certFile,
})
require.NoError(t, err)
require.NotNil(t, srv)
})
}
@@ -983,3 +1072,250 @@ func TestServeCACert_CompoundPEM(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "Shared Test CA", cert.Subject.CommonName)
}
func TestUpstreamProxy(t *testing.T) {
t.Parallel()
tests := []struct {
name string
// passthrough determines whether the request should be tunneled through
// the upstream proxy (true) or MITM'd by aiproxy (false).
// When true, the target domain is NOT in the allowlist.
// When false, the target domain IS in the allowlist.
passthrough bool
// upstreamProxyTLS determines whether the upstream proxy uses TLS.
// When true, aiproxy must be configured with the upstream proxy's CA.
upstreamProxyTLS bool
// buildTargetURL constructs the request URL. For passthrough, it uses
// the final destination URL. For MITM, it uses api.anthropic.com.
buildTargetURL func(finalDestinationURL *url.URL) string
// expectedAIBridgePath is the path aibridge should receive for MITM requests.
expectedAIBridgePath string
}{
{
name: "NonAllowlistedDomain_PassthroughToHTTPUpstreamProxy",
passthrough: true,
upstreamProxyTLS: false,
buildTargetURL: func(finalDestinationURL *url.URL) string {
return fmt.Sprintf("https://%s/passthrough-path", finalDestinationURL.Host)
},
},
{
name: "NonAllowlistedDomain_PassthroughToHTTPSUpstreamProxy",
passthrough: true,
upstreamProxyTLS: true,
buildTargetURL: func(finalDestinationURL *url.URL) string {
return fmt.Sprintf("https://%s/passthrough-path", finalDestinationURL.Host)
},
},
{
name: "AllowlistedDomain_MITMByAIProxy",
passthrough: false,
upstreamProxyTLS: false,
buildTargetURL: func(_ *url.URL) string {
return "https://api.anthropic.com:443/v1/messages"
},
expectedAIBridgePath: "/api/v2/aibridge/anthropic/v1/messages",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Track requests received by each component to verify the flow.
var (
upstreamProxyCONNECTReceived bool
upstreamProxyCONNECTHost string
finalDestinationReceived bool
finalDestinationPath string
finalDestinationBody string
aibridgeReceived bool
aibridgePath string
aibridgeAuthHeader string
aibridgeBody string
)
// Create mock final destination server representing the actual target:
// - For passthrough requests, traffic should reach this server.
// - For MITM requests, traffic should NOT reach this server.
finalDestination, finalDestinationURL := newTargetServer(t, func(w http.ResponseWriter, r *http.Request) {
finalDestinationReceived = true
finalDestinationPath = r.URL.Path
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
finalDestinationBody = string(body)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("final destination response"))
})
// Upstream proxy handler: same logic for both HTTP and HTTPS.
upstreamProxyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodConnect {
http.Error(w, "expected CONNECT request", http.StatusBadRequest)
return
}
upstreamProxyCONNECTReceived = true
upstreamProxyCONNECTHost = r.Host
// Connect to the mock final destination server.
targetConn, err := net.Dial("tcp", finalDestinationURL.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer targetConn.Close()
// Hijack the connection to take over the raw TCP socket.
// After responding "200 Connection Established", the proxy stops being
// an HTTP server and becomes a transparent tunnel that copies bytes
// bidirectionally. The http package can't handle this mode, so we
// hijack and manage the connection ourselves.
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
return
}
defer clientConn.Close()
// Send 200 Connection Established to signal tunnel is ready.
_, _ = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
// Copy data bidirectionally between aiproxy and final destination.
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, _ = io.Copy(targetConn, clientConn)
}()
go func() {
defer wg.Done()
_, _ = io.Copy(clientConn, targetConn)
}()
wg.Wait()
})
// Create upstream proxy: HTTP or HTTPS based on test case.
var upstreamProxy *httptest.Server
var upstreamProxyCAFile string
if tt.upstreamProxyTLS {
upstreamProxy = httptest.NewTLSServer(upstreamProxyHandler)
// Write the upstream proxy's CA cert to a temp file for aiproxy to trust.
upstreamProxyCAFile = filepath.Join(t.TempDir(), "upstream-proxy-ca.pem")
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: upstreamProxy.Certificate().Raw,
})
err := os.WriteFile(upstreamProxyCAFile, certPEM, 0o600)
require.NoError(t, err)
} else {
upstreamProxy = httptest.NewServer(upstreamProxyHandler)
}
t.Cleanup(upstreamProxy.Close)
// Create a mock aibridged server:
// - For passthrough requests, traffic should NOT reach this server.
// - For MITM requests, aiproxy rewrites the URL and forwards here.
aibridgeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
aibridgeReceived = true
aibridgePath = r.URL.Path
aibridgeAuthHeader = r.Header.Get("Authorization")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
aibridgeBody = string(body)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("aibridge response"))
}))
t.Cleanup(aibridgeServer.Close)
// Build the target URL for this test case.
targetURL := tt.buildTargetURL(finalDestinationURL)
parsedTargetURL, err := url.Parse(targetURL)
require.NoError(t, err)
// Configure allowlist based on test case:
// - For passthrough, api.anthropic.com is in allowlist, but we target a different host.
// - For MITM, api.anthropic.com must be in the allowlist.
domainAllowlist := []string{"api.anthropic.com"}
// Create aiproxy with upstream proxy configured.
proxyOpts := []testProxyOption{
withCoderAccessURL(aibridgeServer.URL),
withDomainAllowlist(domainAllowlist...),
withUpstreamProxy(upstreamProxy.URL),
withAllowedPorts("80", "443", finalDestinationURL.Port(), parsedTargetURL.Port()),
}
if upstreamProxyCAFile != "" {
proxyOpts = append(proxyOpts, withUpstreamProxyCA(upstreamProxyCAFile))
}
srv := newTestProxy(t, proxyOpts...)
// Configure certificate trust based on test case:
// - For passthrough: client trusts final destination's CA.
// - For MITM: client trusts aiproxy's CA (fake certs).
var certPool *x509.CertPool
if tt.passthrough {
certPool = x509.NewCertPool()
certPool.AddCert(finalDestination.Certificate())
} else {
certPool = getProxyCertPool(t)
}
// Create HTTP client configured to use aiproxy.
client := newProxyClient(t, srv, makeProxyAuthHeader("test-coder-token"), certPool)
// Make request through aiproxy.
requestBody := `{"test": "data", "foo": "bar"}`
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, targetURL, strings.NewReader(requestBody))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
// Verify the request flow based on test case.
if tt.passthrough {
require.True(t, upstreamProxyCONNECTReceived,
"upstream proxy should receive CONNECT for non-allowlisted domain")
require.Equal(t, finalDestinationURL.Host, upstreamProxyCONNECTHost,
"upstream proxy should receive CONNECT to correct host")
require.True(t, finalDestinationReceived,
"final destination should receive the passthrough request")
require.Equal(t, parsedTargetURL.Path, finalDestinationPath,
"final destination should receive correct path")
require.Equal(t, requestBody, finalDestinationBody,
"final destination should receive the exact request body")
require.False(t, aibridgeReceived,
"aibridge should NOT receive request for non-allowlisted domain")
} else {
require.False(t, upstreamProxyCONNECTReceived,
"upstream proxy should NOT receive CONNECT for allowlisted domain")
require.True(t, aibridgeReceived,
"aibridge should receive the MITM'd request")
require.Equal(t, tt.expectedAIBridgePath, aibridgePath,
"aibridge should receive rewritten path")
require.Equal(t, "Bearer test-coder-token", aibridgeAuthHeader,
"aibridge should receive auth header extracted from proxy auth")
require.Equal(t, requestBody, aibridgeBody,
"aibridge should receive the exact request body")
require.False(t, finalDestinationReceived,
"final destination should NOT receive request for allowlisted domain")
}
})
}
}
+2
View File
@@ -23,6 +23,8 @@ func newAIBridgeProxyDaemon(coderAPI *coderd.API) (*aibridgeproxyd.Server, error
CertFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.CertFile.String(),
KeyFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.KeyFile.String(),
DomainAllowlist: coderAPI.DeploymentValues.AI.BridgeProxyConfig.DomainAllowlist.Value(),
UpstreamProxy: coderAPI.DeploymentValues.AI.BridgeProxyConfig.UpstreamProxy.String(),
UpstreamProxyCA: coderAPI.DeploymentValues.AI.BridgeProxyConfig.UpstreamProxyCA.String(),
})
if err != nil {
return nil, xerrors.Errorf("failed to start in-memory aibridgeproxy daemon: %w", err)
+11
View File
@@ -166,6 +166,17 @@ AI BRIDGE PROXY OPTIONS:
--aibridge-proxy-listen-addr string, $CODER_AIBRIDGE_PROXY_LISTEN_ADDR (default: :8888)
The address the AI Bridge Proxy will listen on.
--aibridge-proxy-upstream string, $CODER_AIBRIDGE_PROXY_UPSTREAM
URL of an upstream HTTP proxy to chain passthrough (non-allowlisted)
requests through. Format: http://[user:pass@]host:port or
https://[user:pass@]host:port.
--aibridge-proxy-upstream-ca string, $CODER_AIBRIDGE_PROXY_UPSTREAM_CA
Path to a PEM-encoded CA certificate to trust for the upstream proxy's
TLS connection. Only needed for HTTPS upstream proxies with
certificates not trusted by the system. If not provided, the system
certificate pool is used.
CLIENT OPTIONS:
These options change the behavior of how clients interact with the Coder.
Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI.
+2
View File
@@ -73,6 +73,8 @@ export interface AIBridgeProxyConfig {
readonly cert_file: string;
readonly key_file: string;
readonly domain_allowlist: string;
readonly upstream_proxy: string;
readonly upstream_proxy_ca: string;
}
// From codersdk/aibridge.go