mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 11:39:57 +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).
141 lines
5.8 KiB
Go
141 lines
5.8 KiB
Go
//go:build e2e
|
|
|
|
package agentnetwork
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/e2e/harness"
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
)
|
|
|
|
// TestProviderSkipTLSVerification proves skip_tls_verification is per-provider:
|
|
// two providers share one self-signed upstream, one skipping TLS verification
|
|
// and one not. The skip=true provider's chat reaches the upstream and returns
|
|
// 200; the skip=false provider's chat fails at the TLS handshake — same
|
|
// upstream, opposite outcome. This is the behaviour a target-level flag could
|
|
// not give, since all of an account's providers share one synthesised target.
|
|
func TestProviderSkipTLSVerification(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
|
defer cancel()
|
|
|
|
up, err := harness.StartFakeUpstream(ctx, srv)
|
|
require.NoError(t, err, "start self-signed upstream")
|
|
t.Cleanup(func() { _ = up.Terminate(context.Background()) })
|
|
|
|
grp, err := srv.API().Groups.Create(ctx, api.PostApiGroupsJSONRequestBody{Name: "e2e-skiptls"})
|
|
require.NoError(t, err, "create group")
|
|
t.Cleanup(func() { _ = srv.API().Groups.Delete(context.Background(), grp.Id) })
|
|
|
|
ephemeral := false
|
|
sk, err := srv.API().SetupKeys.Create(ctx, api.PostApiSetupKeysJSONRequestBody{
|
|
Name: "e2e-skiptls-client",
|
|
Type: "reusable",
|
|
ExpiresIn: 86400,
|
|
UsageLimit: 0,
|
|
AutoGroups: []string{grp.Id},
|
|
Ephemeral: &ephemeral,
|
|
})
|
|
require.NoError(t, err, "mint setup key")
|
|
require.NotEmpty(t, sk.Key, "setup key plaintext")
|
|
|
|
const (
|
|
insecureModel = "insecure-model"
|
|
secureModel = "secure-model"
|
|
)
|
|
|
|
// Two providers on the SAME self-signed upstream, distinguished only by their
|
|
// skip_tls_verification and a unique model string so the router picks each
|
|
// unambiguously.
|
|
newReq := func(name, model string, skip bool) api.AgentNetworkProviderRequest {
|
|
key := "sk-dummy-e2e"
|
|
return api.AgentNetworkProviderRequest{
|
|
Name: name,
|
|
ProviderId: "openai_api",
|
|
UpstreamUrl: up.URL,
|
|
ApiKey: &key,
|
|
Enabled: ptr(true),
|
|
SkipTlsVerification: ptr(skip),
|
|
Models: &[]api.AgentNetworkProviderModel{
|
|
{Id: model, InputPer1k: 0.001, OutputPer1k: 0.002},
|
|
},
|
|
}
|
|
}
|
|
|
|
// First create bootstraps the account cluster.
|
|
insecureReq := newReq("skip-tls", insecureModel, true)
|
|
insecureReq.BootstrapCluster = ptr(harness.AgentNetworkCluster)
|
|
insecureProv, err := srv.CreateProvider(ctx, insecureReq)
|
|
require.NoError(t, err, "create skip-tls provider")
|
|
t.Cleanup(func() { _ = srv.DeleteProvider(context.Background(), insecureProv.Id) })
|
|
require.True(t, insecureProv.SkipTlsVerification, "response must echo skip_tls_verification=true")
|
|
|
|
secureProv, err := srv.CreateProvider(ctx, newReq("verify-tls", secureModel, false))
|
|
require.NoError(t, err, "create verify-tls provider")
|
|
t.Cleanup(func() { _ = srv.DeleteProvider(context.Background(), secureProv.Id) })
|
|
require.False(t, secureProv.SkipTlsVerification, "response must echo skip_tls_verification=false")
|
|
|
|
enabled := true
|
|
pol, err := srv.CreatePolicy(ctx, api.AgentNetworkPolicyRequest{
|
|
Name: "e2e-skiptls-allow",
|
|
Enabled: &enabled,
|
|
SourceGroups: []string{grp.Id},
|
|
DestinationProviderIds: []string{insecureProv.Id, secureProv.Id},
|
|
})
|
|
require.NoError(t, err, "create policy")
|
|
t.Cleanup(func() { _ = srv.DeletePolicy(context.Background(), pol.Id) })
|
|
|
|
settings, err := srv.GetSettings(ctx)
|
|
require.NoError(t, err, "read settings")
|
|
require.NotEmpty(t, settings.Endpoint, "endpoint must be assigned")
|
|
|
|
proxyToken, err := srv.CreateProxyTokenCLI(ctx, "e2e-skiptls-proxy")
|
|
require.NoError(t, err, "mint proxy token")
|
|
px, err := harness.StartProxy(ctx, srv, proxyToken)
|
|
require.NoError(t, err, "start proxy")
|
|
t.Cleanup(func() { _ = px.Terminate(context.Background()) })
|
|
|
|
cl, err := harness.StartClient(ctx, srv, sk.Key)
|
|
require.NoError(t, err, "start client")
|
|
t.Cleanup(func() { _ = cl.Terminate(context.Background()) })
|
|
|
|
require.NoError(t, cl.WaitConnected(ctx, 90*time.Second), "client must connect to management")
|
|
if err := cl.WaitProxyPeer(ctx, 180*time.Second); err != nil {
|
|
t.Fatalf("client did not see the proxy peer: %v\n=== proxy logs ===\n%s", err, px.Logs(context.Background()))
|
|
}
|
|
proxyIP, err := cl.ResolveProxyIP(ctx, settings.Endpoint)
|
|
require.NoError(t, err, "resolve endpoint to proxy IP")
|
|
|
|
// Positive: skip=true reaches the self-signed upstream. Retry to absorb
|
|
// tunnel/DNS jitter on the first call; success also proves the path works.
|
|
var code int
|
|
var body string
|
|
deadline := time.Now().Add(90 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
c, b, cerr := cl.Chat(ctx, settings.Endpoint, proxyIP, harness.WireChat, insecureModel, "Reply with exactly: pong", "e2e-skiptls-insecure")
|
|
if cerr == nil {
|
|
code, body = c, b
|
|
if code == 200 {
|
|
break
|
|
}
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
require.Equal(t, 200, code,
|
|
"skip_tls_verification=true must reach the self-signed upstream; body: %s\n=== upstream logs ===\n%s\n=== proxy logs ===\n%s",
|
|
body, up.Logs(context.Background()), px.Logs(context.Background()))
|
|
|
|
// Negative: skip=false must fail the TLS handshake to the SAME upstream. The
|
|
// path is already proven working, so a non-200 here is the cert rejection.
|
|
secureCode, secureBody, cerr := cl.Chat(ctx, settings.Endpoint, proxyIP, harness.WireChat, secureModel, "Reply with exactly: pong", "e2e-skiptls-secure")
|
|
require.NoError(t, cerr, "the chat call itself must complete (proxy returns an error status, not a transport error)")
|
|
require.NotEqual(t, 200, secureCode,
|
|
"skip_tls_verification=false must NOT reach the self-signed upstream; got %d, body: %s", secureCode, secureBody)
|
|
require.GreaterOrEqual(t, secureCode, 500,
|
|
"a TLS verification failure should surface as a 5xx from the proxy; got %d, body: %s", secureCode, secureBody)
|
|
}
|