//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) }