mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: avoid terminology collision with aibridge by renaming passthrough to tunneled (#21562)
## Description Renames "passthrough" to "tunneled" in aiproxy to avoid terminology collision with aibridge, which has its own passthrough concept. Follow-up from: https://github.com/coder/coder/pull/21512#discussion_r2698231778 --------- Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
co-authored by
Danny Kopping
parent
08343a7a9f
commit
a002fbbae6
+1
-1
@@ -166,7 +166,7 @@ AI BRIDGE PROXY OPTIONS:
|
||||
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)
|
||||
URL of an upstream HTTP proxy to chain tunneled (non-allowlisted)
|
||||
requests through. Format: http://[user:pass@]host:port or
|
||||
https://[user:pass@]host:port.
|
||||
|
||||
|
||||
+1
-1
@@ -798,7 +798,7 @@ aibridgeproxy:
|
||||
domain_allowlist:
|
||||
- api.anthropic.com
|
||||
- api.openai.com
|
||||
# URL of an upstream HTTP proxy to chain passthrough (non-allowlisted) requests
|
||||
# URL of an upstream HTTP proxy to chain tunneled (non-allowlisted) requests
|
||||
# through. Format: http://[user:pass@]host:port or https://[user:pass@]host:port.
|
||||
# (default: <unset>, type: string)
|
||||
upstream_proxy: ""
|
||||
|
||||
@@ -3549,7 +3549,7 @@ Write out the current server config as YAML to stdout.`,
|
||||
},
|
||||
{
|
||||
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.",
|
||||
Description: "URL of an upstream HTTP proxy to chain tunneled (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,
|
||||
|
||||
Generated
+1
-1
@@ -1897,7 +1897,7 @@ Path to the CA private key file for AI Bridge Proxy.
|
||||
| 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.
|
||||
URL of an upstream HTTP proxy to chain tunneled (non-allowlisted) requests through. Format: http://[user:pass@]host:port or https://[user:pass@]host:port.
|
||||
|
||||
### --aibridge-proxy-upstream-ca
|
||||
|
||||
|
||||
@@ -75,8 +75,8 @@ 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
|
||||
// UpstreamProxy is the URL of an upstream HTTP proxy to chain tunneled
|
||||
// (non-allowlisted) requests through. If empty, tunneled requests connect
|
||||
// directly to their destinations.
|
||||
// Format: http://[user:pass@]host:port or https://[user:pass@]host:port
|
||||
UpstreamProxy string
|
||||
@@ -152,7 +152,7 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error)
|
||||
return nil, xerrors.Errorf("failed to load system certificate pool: %w", err)
|
||||
}
|
||||
|
||||
// Configure upstream proxy for passthrough (non-allowlisted) requests.
|
||||
// Configure upstream proxy for tunneled (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
|
||||
@@ -164,7 +164,7 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error)
|
||||
return nil, xerrors.Errorf("invalid upstream proxy URL %q: %w", opts.UpstreamProxy, err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "configuring upstream proxy for passthrough requests",
|
||||
logger.Info(ctx, "configuring upstream proxy for tunneled requests",
|
||||
slog.F("upstream", upstreamURL.Host),
|
||||
)
|
||||
|
||||
@@ -196,7 +196,7 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Configure passthrough CONNECT requests to go through upstream proxy.
|
||||
// Configure tunneled 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)
|
||||
@@ -223,7 +223,7 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error)
|
||||
srv.authMiddleware,
|
||||
)
|
||||
|
||||
// Handle decrypted requests: route to aibridged for known AI providers, or passthrough to original destination.
|
||||
// Handle decrypted requests: route to aibridged for known AI providers, or tunnel to original destination.
|
||||
proxy.OnRequest().DoFunc(srv.handleRequest)
|
||||
|
||||
// Create listener first so we can get the actual address.
|
||||
@@ -474,7 +474,7 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
|
||||
slog.F("method", req.Method),
|
||||
slog.F("path", originalPath),
|
||||
)
|
||||
// Passthrough to the original destination.
|
||||
// Tunnel (forward) directly to the original destination (no aibridge routing for this host).
|
||||
return req, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ func getProxyCertPool(t *testing.T) *x509.CertPool {
|
||||
// newProxyClient creates an HTTP client configured to use the proxy.
|
||||
// It adds a Proxy-Authorization header with the provided token for authentication.
|
||||
// The certPool parameter specifies which certificates the client should trust.
|
||||
// For MITM'd requests, use the proxy's CA. For passthrough, use the target server's cert.
|
||||
// For MITM'd requests, use the proxy's CA. For tunneled requests, use the target server's cert.
|
||||
func newProxyClient(t *testing.T, srv *aibridgeproxyd.Server, proxyAuth string, certPool *x509.CertPool) *http.Client {
|
||||
t.Helper()
|
||||
|
||||
@@ -585,17 +585,17 @@ func TestProxy_CertCaching(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
domainAllowlist []string
|
||||
passthrough bool
|
||||
tunneled bool
|
||||
}{
|
||||
{
|
||||
name: "AllowlistedDomainCached",
|
||||
domainAllowlist: nil, // will use targetURL.Hostname()
|
||||
passthrough: false,
|
||||
tunneled: false,
|
||||
},
|
||||
{
|
||||
name: "NonAllowlistedDomainNotCached",
|
||||
domainAllowlist: []string{"other.example.com"},
|
||||
passthrough: true,
|
||||
tunneled: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -624,13 +624,13 @@ func TestProxy_CertCaching(t *testing.T) {
|
||||
withDomainAllowlist(domainAllowlist...),
|
||||
)
|
||||
|
||||
// Build the cert pool for the client to trust.
|
||||
// Build the cert pool for the client to trust:
|
||||
// - For tunneled requests, the client connects directly to the target server
|
||||
// through a tunnel, so it needs to trust the target's self-signed certificate.
|
||||
// - For MITM'd requests, the client connects through the proxy which generates
|
||||
// certificates signed by our test CA, so it needs to trust the proxy's CA.
|
||||
// - For passthrough requests, the client connects directly to the target server
|
||||
// through a tunnel, so it needs to trust the target's self-signed certificate.
|
||||
var certPool *x509.CertPool
|
||||
if tt.passthrough {
|
||||
if tt.tunneled {
|
||||
certPool = x509.NewCertPool()
|
||||
certPool.AddCert(targetServer.Certificate())
|
||||
} else {
|
||||
@@ -653,7 +653,7 @@ func TestProxy_CertCaching(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.passthrough {
|
||||
if tt.tunneled {
|
||||
// Certificate should NOT have been cached since request was tunneled.
|
||||
require.Equal(t, 1, genCalls, "certificate should NOT have been cached for non-allowlisted domain")
|
||||
} else {
|
||||
@@ -803,8 +803,8 @@ func TestProxy_MITM(t *testing.T) {
|
||||
name string
|
||||
domainAllowlist []string
|
||||
allowedPorts []string
|
||||
buildTargetURL func(passthroughURL *url.URL) (string, error)
|
||||
passthrough bool
|
||||
buildTargetURL func(tunneledURL *url.URL) (string, error)
|
||||
tunneled bool
|
||||
noAIBridgeRouting bool
|
||||
expectedPath string
|
||||
}{
|
||||
@@ -845,25 +845,25 @@ func TestProxy_MITM(t *testing.T) {
|
||||
expectedPath: "/api/v2/aibridge/openai/v1/chat/completions",
|
||||
},
|
||||
{
|
||||
name: "PassthroughUnknownHost",
|
||||
name: "TunneledUnknownHost",
|
||||
domainAllowlist: []string{"other.example.com"},
|
||||
allowedPorts: nil, // will use passthroughURL.Port()
|
||||
buildTargetURL: func(passthroughURL *url.URL) (string, error) {
|
||||
return url.JoinPath(passthroughURL.String(), "/some/path")
|
||||
allowedPorts: nil, // will use tunneledURL.Port()
|
||||
buildTargetURL: func(tunneledURL *url.URL) (string, error) {
|
||||
return url.JoinPath(tunneledURL.String(), "/some/path")
|
||||
},
|
||||
passthrough: true,
|
||||
tunneled: true,
|
||||
},
|
||||
// The host is MITM'd but has no provider mapping.
|
||||
// The request is decrypted but passed through to the original destination
|
||||
// The request is decrypted but forwarded to the original destination
|
||||
// instead of being routed to aibridge.
|
||||
{
|
||||
name: "MitmdWithoutAIBridgeRouting",
|
||||
domainAllowlist: nil, // will use passthroughURL.Hostname()
|
||||
allowedPorts: nil, // will use passthroughURL.Port()
|
||||
buildTargetURL: func(passthroughURL *url.URL) (string, error) {
|
||||
return url.JoinPath(passthroughURL.String(), "/some/path")
|
||||
domainAllowlist: nil, // will use tunneledURL.Hostname()
|
||||
allowedPorts: nil, // will use tunneledURL.Port()
|
||||
buildTargetURL: func(tunneledURL *url.URL) (string, error) {
|
||||
return url.JoinPath(tunneledURL.String(), "/some/path")
|
||||
},
|
||||
passthrough: false,
|
||||
tunneled: false,
|
||||
noAIBridgeRouting: true,
|
||||
},
|
||||
}
|
||||
@@ -885,22 +885,22 @@ func TestProxy_MITM(t *testing.T) {
|
||||
}))
|
||||
t.Cleanup(func() { aibridgedServer.Close() })
|
||||
|
||||
// Create a mock target server for passthrough tests.
|
||||
passthroughServer, passthroughURL := newTargetServer(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
// Create a mock target server for tunneled tests.
|
||||
tunneledServer, tunneledURL := newTargetServer(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hello from passthrough"))
|
||||
_, _ = w.Write([]byte("hello from tunneled"))
|
||||
})
|
||||
|
||||
// Configure allowed ports.
|
||||
allowedPorts := tt.allowedPorts
|
||||
if allowedPorts == nil {
|
||||
allowedPorts = []string{passthroughURL.Port()}
|
||||
allowedPorts = []string{tunneledURL.Port()}
|
||||
}
|
||||
|
||||
// Configure domain allowlist.
|
||||
domainAllowlist := tt.domainAllowlist
|
||||
if domainAllowlist == nil {
|
||||
domainAllowlist = []string{passthroughURL.Hostname()}
|
||||
domainAllowlist = []string{tunneledURL.Hostname()}
|
||||
}
|
||||
|
||||
// Start the proxy server pointing to our mock aibridged.
|
||||
@@ -911,18 +911,18 @@ func TestProxy_MITM(t *testing.T) {
|
||||
)
|
||||
|
||||
// Build the target URL:
|
||||
targetURL, err := tt.buildTargetURL(passthroughURL)
|
||||
targetURL, err := tt.buildTargetURL(tunneledURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build the cert pool for the client to trust.
|
||||
// Build the cert pool for the client to trust:
|
||||
// - For tunneled requests, the client connects directly to the target server
|
||||
// through a tunnel, so it needs to trust the target's self-signed certificate.
|
||||
// - For MITM'd requests, the client connects through the proxy which generates
|
||||
// certificates signed by our test CA, so it needs to trust the proxy's CA.
|
||||
// - For passthrough requests, the client connects directly to the target server
|
||||
// through a tunnel, so it needs to trust the target's self-signed certificate.
|
||||
var certPool *x509.CertPool
|
||||
if tt.passthrough {
|
||||
if tt.tunneled {
|
||||
certPool = x509.NewCertPool()
|
||||
certPool.AddCert(passthroughServer.Certificate())
|
||||
certPool.AddCert(tunneledServer.Certificate())
|
||||
} else {
|
||||
certPool = getProxyCertPool(t)
|
||||
}
|
||||
@@ -941,11 +941,11 @@ func TestProxy_MITM(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
if tt.passthrough || tt.noAIBridgeRouting {
|
||||
if tt.tunneled || tt.noAIBridgeRouting {
|
||||
// Verify request went to target server, not aibridged.
|
||||
require.Equal(t, "hello from passthrough", string(body))
|
||||
require.Empty(t, receivedPath, "aibridged should not receive passthrough requests")
|
||||
require.Empty(t, receivedAuth, "passthrough requests are not authenticated by the proxy")
|
||||
require.Equal(t, "hello from tunneled", string(body))
|
||||
require.Empty(t, receivedPath, "aibridged should not receive tunneled requests")
|
||||
require.Empty(t, receivedAuth, "tunneled requests are not authenticated by the proxy")
|
||||
} else {
|
||||
// Verify the request was routed to aibridged correctly.
|
||||
require.Equal(t, "hello from aibridged", string(body))
|
||||
@@ -1078,39 +1078,39 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
// passthrough determines whether the request should be tunneled through
|
||||
// tunneled 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
|
||||
tunneled 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
|
||||
// buildTargetURL constructs the request URL. For tunneled requests, 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,
|
||||
name: "NonAllowlistedDomain_TunneledToHTTPUpstreamProxy",
|
||||
tunneled: true,
|
||||
upstreamProxyTLS: false,
|
||||
buildTargetURL: func(finalDestinationURL *url.URL) string {
|
||||
return fmt.Sprintf("https://%s/passthrough-path", finalDestinationURL.Host)
|
||||
return fmt.Sprintf("https://%s/tunneled-path", finalDestinationURL.Host)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NonAllowlistedDomain_PassthroughToHTTPSUpstreamProxy",
|
||||
passthrough: true,
|
||||
name: "NonAllowlistedDomain_TunneledToHTTPSUpstreamProxy",
|
||||
tunneled: true,
|
||||
upstreamProxyTLS: true,
|
||||
buildTargetURL: func(finalDestinationURL *url.URL) string {
|
||||
return fmt.Sprintf("https://%s/passthrough-path", finalDestinationURL.Host)
|
||||
return fmt.Sprintf("https://%s/tunneled-path", finalDestinationURL.Host)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AllowlistedDomain_MITMByAIProxy",
|
||||
passthrough: false,
|
||||
tunneled: false,
|
||||
upstreamProxyTLS: false,
|
||||
buildTargetURL: func(_ *url.URL) string {
|
||||
return "https://api.anthropic.com:443/v1/messages"
|
||||
@@ -1137,7 +1137,7 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
)
|
||||
|
||||
// Create mock final destination server representing the actual target:
|
||||
// - For passthrough requests, traffic should reach this server.
|
||||
// - For tunneled 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
|
||||
@@ -1223,7 +1223,7 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
t.Cleanup(upstreamProxy.Close)
|
||||
|
||||
// Create a mock aibridged server:
|
||||
// - For passthrough requests, traffic should NOT reach this server.
|
||||
// - For tunneled 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
|
||||
@@ -1246,7 +1246,7 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
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 tunneled requests, 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"}
|
||||
|
||||
@@ -1263,10 +1263,10 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
srv := newTestProxy(t, proxyOpts...)
|
||||
|
||||
// Configure certificate trust based on test case:
|
||||
// - For passthrough: client trusts final destination's CA.
|
||||
// - For tunneled requests: client trusts final destination's CA.
|
||||
// - For MITM: client trusts aiproxy's CA (fake certs).
|
||||
var certPool *x509.CertPool
|
||||
if tt.passthrough {
|
||||
if tt.tunneled {
|
||||
certPool = x509.NewCertPool()
|
||||
certPool.AddCert(finalDestination.Certificate())
|
||||
} else {
|
||||
@@ -1289,13 +1289,13 @@ func TestUpstreamProxy(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Verify the request flow based on test case.
|
||||
if tt.passthrough {
|
||||
if tt.tunneled {
|
||||
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")
|
||||
"final destination should receive the tunneled request")
|
||||
require.Equal(t, parsedTargetURL.Path, finalDestinationPath,
|
||||
"final destination should receive correct path")
|
||||
require.Equal(t, requestBody, finalDestinationBody,
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ AI BRIDGE PROXY OPTIONS:
|
||||
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)
|
||||
URL of an upstream HTTP proxy to chain tunneled (non-allowlisted)
|
||||
requests through. Format: http://[user:pass@]host:port or
|
||||
https://[user:pass@]host:port.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user