Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions e2e/harness/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ package harness
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go"
tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/wait"
Expand All @@ -24,6 +29,7 @@ const (
// pulled as a published image; a bare tag is built under that name.
defaultProxyImage = "netbird-reverse-proxy:e2e"
proxyAlias = "proxy"
proxyHTTPSPort = "443/tcp"

// AgentNetworkCluster is the proxy cluster the e2e provider bootstraps and
// the proxy serves. It must equal the management's exposed domain
Expand All @@ -34,8 +40,9 @@ const (

// Proxy is a running agent-network gateway (netbird proxy) container.
type Proxy struct {
container testcontainers.Container
workDir string
container testcontainers.Container
workDir string
httpsAddress string
}

// StartProxy builds the proxy image and runs it on the combined server's
Expand Down Expand Up @@ -71,6 +78,7 @@ func StartProxy(ctx context.Context, c *Combined, proxyToken string, envOverride

req := testcontainers.ContainerRequest{
Image: proxyImage,
ExposedPorts: []string{proxyHTTPSPort},
Networks: []string{c.network.Name},
NetworkAliases: map[string][]string{c.network.Name: {proxyAlias}},
Env: map[string]string{
Expand Down Expand Up @@ -112,10 +120,74 @@ func StartProxy(ctx context.Context, c *Combined, proxyToken string, envOverride
Started: true,
})
if err != nil {
_ = os.RemoveAll(workDir)
return nil, fmt.Errorf("start proxy container: %w", err)
}

return &Proxy{container: ctr, workDir: workDir}, nil
host, err := ctr.Host(ctx)
if err != nil {
_ = ctr.Terminate(ctx)
_ = os.RemoveAll(workDir)
return nil, fmt.Errorf("proxy container host: %w", err)
}
mapped, err := ctr.MappedPort(ctx, nat.Port(proxyHTTPSPort))
if err != nil {
_ = ctr.Terminate(ctx)
_ = os.RemoveAll(workDir)
return nil, fmt.Errorf("proxy mapped HTTPS port: %w", err)
}

return &Proxy{
container: ctr,
workDir: workDir,
httpsAddress: net.JoinHostPort(host, mapped.Port()),
}, nil
}

// HTTPSGet reaches a public service through the proxy's host-mapped HTTPS
// listener while retaining domain as the request Host and TLS server name.
// Connections are not reused so callers can observe mapping removal without
// an existing connection outliving the route.
func (p *Proxy) HTTPSGet(ctx context.Context, domain, path string, headers http.Header) (int, string, error) {
requestURL := &url.URL{Scheme: "https", Host: domain, Path: path}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
if err != nil {
return 0, "", fmt.Errorf("create proxy request: %w", err)
}
req.Header = headers.Clone()

dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", p.httpsAddress)
},
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true, //nolint:gosec // the e2e proxy intentionally uses a generated self-signed certificate
},
DisableKeepAlives: true,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
}
defer transport.CloseIdleConnections()

client := &http.Client{
Transport: transport,
Timeout: 15 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Do(req)
if err != nil {
return 0, "", fmt.Errorf("request public proxy service: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, "", fmt.Errorf("read public proxy response: %w", err)
}
return resp.StatusCode, string(body), nil
}

// ProxyDebugClient is one per-account embedded client the proxy runs, as the
Expand Down
42 changes: 42 additions & 0 deletions e2e/reverseproxy/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build e2e

// Package reverseproxy contains container-based tests for public reverse-proxy
// services. A real combined management server is shared across package tests;
// each test owns and cleans up its proxy and service resources.
package reverseproxy

import (
"context"
"fmt"
"os"
"testing"
"time"

"github.com/netbirdio/netbird/e2e/harness"
)

var srv *harness.Combined

func TestMain(m *testing.M) {
os.Exit(run(m))
}

func run(m *testing.M) int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()

var err error
srv, err = harness.StartCombined(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "e2e: start combined server: %v\n", err)
return 1
}
defer func() { _ = srv.Terminate(context.Background()) }()

if _, err := srv.Bootstrap(ctx); err != nil {
fmt.Fprintf(os.Stderr, "e2e: bootstrap admin PAT: %v\n", err)
return 1
}

return m.Run()
}
Loading
Loading