mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 19:49:56 +00:00
* [management,proxy] Add per-provider skip_tls_verification for agent-network Let agent-network providers opt into skipping upstream TLS verification for self-hosted / internal gateways behind a private or self-signed cert. - provider: add SkipTLSVerification (persisted via AutoMigrate) with request/response mapping (nil on update preserves, explicit false clears). - openapi: skip_tls_verification on the provider request + response; types regenerated. - synthesizer: carry the flag into the llm_router route config so it reaches the proxy. - proxy: llm_router sets it on the UpstreamRewrite mutation, and the reverse proxy applies roundtrip.WithSkipTLSVerify per selected route when forwarding upstream (the router dials per provider, so a per-target flag alone wouldn't cover it). - tests: synthesizer route config carries the flag, router rewrite propagates it, and the request/response round-trip incl. update semantics. * [e2e] Validate per-provider skip_tls_verification end to end Add a self-signed HTTPS upstream (nginx) to the harness and a test that provisions two providers on that same upstream — one with skip_tls_verification=true, one false — behind one proxy + client. The skip=true provider's chat reaches the upstream (200); the skip=false provider's fails the TLS handshake (5xx). Same upstream, opposite outcome, which proves the flag is honoured per provider (a single target-level flag could not, since all of an account's providers share one synthesised target). * [e2e] WaitProxyPeer: require >=1 connected peer, not exact 1/1 Each proxy container registers a fresh WireGuard key and its peer is not removed on teardown, so proxy peers from earlier tests linger in the account as disconnected. WaitProxyPeer matched the exact string "1/1 Connected", which failed once a second proxy-using test ran in the same package (status "1/2"). Parse the "Peers count: X/Y Connected" line and wait for X>=1 instead: only the live proxy can be connected, and the caller's subsequent chat is the real end-to-end assertion. Fixes the CI failure of TestProviderSkipTLSVerification (runs after TestProvidersMatrix).
108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
//go:build e2e
|
|
|
|
package harness
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
const (
|
|
fakeUpstreamImage = "nginx:alpine"
|
|
fakeUpstreamAlias = "fakeupstream"
|
|
fakeUpstreamPort = "443/tcp"
|
|
)
|
|
|
|
// fakeUpstreamNginxConf serves a canned OpenAI-shaped chat completion for any
|
|
// path over a self-signed certificate, so the proxy reaches it only when the
|
|
// provider opts into skipping TLS verification.
|
|
const fakeUpstreamNginxConf = `pid /tmp/nginx.pid;
|
|
events {}
|
|
http {
|
|
server {
|
|
listen 443 ssl;
|
|
ssl_certificate /certs/tls.crt;
|
|
ssl_certificate_key /certs/tls.key;
|
|
location / {
|
|
default_type application/json;
|
|
return 200 '{"id":"chatcmpl-e2e","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"pong"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}';
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
// FakeUpstream is a self-signed HTTPS server on the combined server's network,
|
|
// used to exercise provider skip_tls_verification: a proxy that verifies the
|
|
// certificate rejects it, one that skips verification reaches it.
|
|
type FakeUpstream struct {
|
|
container testcontainers.Container
|
|
workDir string
|
|
// URL is the upstream URL providers point at (https://<alias>).
|
|
URL string
|
|
}
|
|
|
|
// StartFakeUpstream runs the self-signed upstream on the shared network.
|
|
func StartFakeUpstream(ctx context.Context, c *Combined) (*FakeUpstream, error) {
|
|
workDir, err := os.MkdirTemp("/tmp", "nb-e2e-upstream-*")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create upstream work dir: %w", err)
|
|
}
|
|
// Widen so the (non-root worker) nginx container can traverse the bind mount.
|
|
if err := os.Chmod(workDir, 0o755); err != nil { //nolint:gosec // throwaway e2e cert dir
|
|
return nil, fmt.Errorf("chmod upstream dir: %w", err)
|
|
}
|
|
if err := writeSelfSignedCert(workDir, []string{fakeUpstreamAlias}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(workDir, "nginx.conf"), []byte(fakeUpstreamNginxConf), 0o644); err != nil { //nolint:gosec // non-secret e2e config
|
|
return nil, fmt.Errorf("write nginx conf: %w", err)
|
|
}
|
|
|
|
req := testcontainers.ContainerRequest{
|
|
Image: fakeUpstreamImage,
|
|
ExposedPorts: []string{fakeUpstreamPort},
|
|
Networks: []string{c.network.Name},
|
|
NetworkAliases: map[string][]string{c.network.Name: {fakeUpstreamAlias}},
|
|
Cmd: []string{"nginx", "-c", "/certs/nginx.conf", "-g", "daemon off;"},
|
|
HostConfigModifier: func(hc *container.HostConfig) {
|
|
hc.Binds = append(hc.Binds, workDir+":/certs:ro")
|
|
},
|
|
WaitingFor: wait.ForListeningPort(fakeUpstreamPort).WithStartupTimeout(60 * time.Second),
|
|
}
|
|
|
|
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
ContainerRequest: req,
|
|
Started: true,
|
|
})
|
|
if err != nil {
|
|
_ = os.RemoveAll(workDir)
|
|
return nil, fmt.Errorf("start fake upstream container: %w", err)
|
|
}
|
|
|
|
return &FakeUpstream{container: ctr, workDir: workDir, URL: "https://" + fakeUpstreamAlias}, nil
|
|
}
|
|
|
|
// Logs returns the upstream container logs, for diagnostics on failure.
|
|
func (u *FakeUpstream) Logs(ctx context.Context) string {
|
|
return containerLogs(ctx, u.container)
|
|
}
|
|
|
|
// Terminate stops the upstream container and cleans its work dir.
|
|
func (u *FakeUpstream) Terminate(ctx context.Context) error {
|
|
var err error
|
|
if u.container != nil {
|
|
err = u.container.Terminate(ctx)
|
|
}
|
|
if u.workDir != "" {
|
|
_ = os.RemoveAll(u.workDir)
|
|
}
|
|
return err
|
|
}
|