testenv

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: LGPL-2.1, LGPL-2.1-or-later Imports: 28 Imported by: 0

Documentation

Overview

Package testenv manages the mock openvpn3-core server for integration tests.

The mock server runs as a pre-built binary: set Config.Binary to its path, or set the MOCK_SERVER_BIN environment variable.

Usage:

func TestMain(m *testing.M) {
    srv, err := testenv.Start(testenv.Config{Binary: mockServerPath})
    if err != nil { log.Fatal(err) }
    defer srv.Stop()
    os.Exit(m.Run())
}

The package additionally carries the OpenVPN server matrix — real, version-pinned OpenVPN 2.4/2.5/2.6 servers built from source and driven from a table (see matrix_entries.go, matrix_axes.go and matrix_server.go). It is a separate mechanism from the mock server above, sharing only the Docker and readiness-polling patterns; StartMatrix/Stop mirror Start/Stop.

Tests that need one of these rigs carry the tag that names what they need — docker, mockserver, soak or privileged — so that `go test ./...` with no tag needs no Docker daemon and no fixture binary. See docs/testing.md. This package is test support. It is imported from outside this module by the private provider-sweep tool, which uses the reference oracle to arbitrate its live failures, and that import carries **no API stability promise**: the tool pins a checkout, not a version, and this package changes when the rig needs it to.

Index

Constants

View Source
const (
	// MatrixUsername is the username every AuthUserPass entry expects.
	MatrixUsername = "matrix-user"
	// MatrixPassword is the password every AuthUserPass entry expects.
	MatrixPassword = "matrix-not-a-secret"
)
View Source
const (
	// MatrixServerCN is the common name on every matrix server certificate.
	MatrixServerCN = "matrix-server"
	// MatrixWrongServerCN is a common name no certificate the matrix issues
	// can carry. It shares no substring with MatrixServerCN, so a profile
	// carrying it can be checked for the real name without a spurious hit.
	MatrixWrongServerCN = "no-such-server"
	// MatrixClientCN is the common name on every matrix client certificate.
	MatrixClientCN = "matrix-client"
	// MatrixCACN is the common name on the throwaway CA.
	MatrixCACN = "go-openvpn-matrix-ca"
)

Common names on the ephemeral matrix PKI. The server's is what a CertCheckX509NameMatch entry names in its verify-x509-name directive, and the wrong one is what CertCheckX509NameMismatch names.

View Source
const (
	// OpenVPN24Version is the exact 2.4 release the matrix builds. 2.4
	// predates TLS keying-material-exporter support, so it forces the
	// classic OpenVPN PRF path.
	OpenVPN24Version = "2.4.12"
	// OpenVPN25Version is the exact 2.5 release the matrix builds.
	OpenVPN25Version = "2.5.11"
	// OpenVPN26Version is the exact 2.6 release the matrix builds.
	OpenVPN26Version = "2.6.22"
)

Pinned OpenVPN releases built into the matrix images. Never a floating tag: a moving version makes every captured result irreproducible.

View Source
const (
	// OpenVPN24SHA256 is the SHA-256 of openvpn-2.4.12.tar.gz.
	OpenVPN24SHA256 = "66952d9c95490e5875f04c9f8fa313b5e816d1b7b4d6cda3fb2ff749ad405dee"
	// OpenVPN25SHA256 is the SHA-256 of openvpn-2.5.11.tar.gz.
	OpenVPN25SHA256 = "7e2672119bd4639819d560f332a8b9b7e28f562425c77899f36d419fe4265f56"
	// OpenVPN26SHA256 is the SHA-256 of openvpn-2.6.22.tar.gz.
	OpenVPN26SHA256 = "f46df740f05f86020137a41cfc8814352391cf861ed57f57b4e815cb97c1d2cf"
)

SHA-256 of each pinned upstream release tarball, verified during the image build. The values were cross-checked against the detached OpenPGP signatures published next to the tarballs.

View Source
const (
	// OpenVPN24Base is the pinned base image for the 2.4 matrix image.
	OpenVPN24Base = "debian:trixie-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f"
	// OpenVPN25Base is the pinned base image for the 2.5 matrix image.
	OpenVPN25Base = "debian:trixie-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f"
	// OpenVPN26Base is the pinned base image for the 2.6 matrix image.
	OpenVPN26Base = "debian:trixie-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f"
)

Base images, pinned by manifest digest. 2.4 and 2.5 link an OpenSSL 1.1.1 built from source — 2.4 does not build against OpenSSL 3.x — and 2.6 the distro's 3.x.

View Source
const ContainerPort = 1194

ContainerPort is the port the server listens on inside the container. The host-side port is allocated per run and published onto it.

View Source
const DefaultOracleHandshakeWindow = 30 * time.Second

DefaultOracleHandshakeWindow is the value given to openvpn's --hand-window, which is how long the client waits for the control channel to come up before declaring "TLS key negotiation failed to occur within N seconds". OpenVPN's own default is 60s; halving it halves the cost of the commonest failure mode without changing its classification, since the timeout is the verdict either way, only sooner.

View Source
const DefaultOracleTimeout = 120 * time.Second

DefaultOracleTimeout bounds one oracle run end to end, including container start. It has to exceed the handshake window with room for several remotes.

View Source
const DefaultOracleVersion = V24

DefaultOracleVersion is the pinned OpenVPN build RunOracle uses when OracleOptions.Version is empty.

2.4 rather than the newest, because the two ways of being wrong are not symmetric: a client too new for the config refuses it outright and the config lands in the "both fail" cell, silently excluded as a bad config that hides one of our gaps, while one too old fails where we succeed and lands in the "surprising" cell, which gets investigated. 2.4 also forces the classic PRF path a pre-2.6 deployment needs. A ClassConfig verdict at StageParse is the first of those — the reference client refused the profile itself — and is the signal to re-run with a newer Version rather than a statement about the endpoint.

View Source
const MatrixCAFile = "ca.crt"

MatrixCAFile is the base name of the CA file a CAFile entry's client profile names instead of carrying an inline <ca> block. Like MatrixCredentialsFile it is both a bundle entry name and an OracleOptions.Files key, so a caller assembling either by hand has to spell it the same way.

View Source
const MatrixCredentialsFile = "auth.txt"

MatrixCredentialsFile is the base name of the credentials file a containerised stock OpenVPN client reads for an AuthUserPass entry. It is exported because overriding it is how a deliberate wrong-password run is built: an OracleOptions.Files entry under this name replaces the one MatrixServer.OracleOptions supplies.

View Source
const MatrixImageRepo = "go-openvpn-test/openvpn-server"

MatrixImageRepo is the local-only image repository the matrix server images are tagged into. Images are built by `make matrix-images` and never pushed.

View Source
const MatrixLabel = "net.bngs.goopenvpn.testenv=matrix"

MatrixLabel is applied to every container StartMatrix creates, so strays can be found and removed with `make matrix-clean`.

View Source
const MatrixReadyTimeout = 30 * time.Second

MatrixReadyTimeout bounds how long StartMatrix waits for the server to log "Initialization Sequence Completed".

Variables

View Source
var (
	// ErrUnimplemented is returned for a matrix entry the rig cannot bring
	// up yet. Enumerating tests should skip on this, not fail. The wrapped
	// message is MatrixEntry.SkipReason.
	ErrUnimplemented = errors.New("testenv: matrix entry not implemented")

	// ErrImageMissing is returned when the pinned server image for an entry
	// has not been built. Its message names the make target that builds it.
	ErrImageMissing = errors.New("testenv: matrix server image not built")
)

Sentinel errors returned by StartMatrix. Tests are expected to distinguish "this entry is not implemented yet" (skip) from "the rig is broken" (fail).

Functions

func EntryNames

func EntryNames() []string

EntryNames returns every matrix entry name in sorted order.

func ImageFor

func ImageFor(v ServerVersion) (string, bool)

ImageFor returns the local image tag for a server series, for example "go-openvpn-test/openvpn-server:2.4.12". The tag always carries the exact upstream version; there is deliberately no "latest" tag. The boolean is false for a series the matrix does not cover.

func OracleRuleNames

func OracleRuleNames() []string

OracleRuleNames returns every rule name ClassifyReferenceLog can report, in no particular order.

Classification.Rule is documented as stable and safe to aggregate on, so a name recorded elsewhere in the tree — MatrixEntry.ReferenceRejects, a report's vocabulary — can be checked against the set that exists rather than the set someone remembered.

func PinnedVersion

func PinnedVersion(v ServerVersion) (string, bool)

PinnedVersion returns the exact upstream OpenVPN release built for a series. The boolean is false for a series the matrix does not cover.

func TailLines

func TailLines(s string, n int) string

TailLines returns at most n trailing lines of s.

Types

type AddressFamily

type AddressFamily int

AddressFamily is the transport address family the server listens on.

const (
	// AFInet listens on IPv4 only and pushes an IPv4 tunnel.
	AFInet AddressFamily = iota
	// AFInet6 listens on an IPv6 socket and additionally pushes an IPv6
	// tunnel. Only the IPv6 host address is published.
	AFInet6
	// AFDual listens on an IPv6 socket accepting v4-mapped connections and
	// pushes both an IPv4 and an IPv6 tunnel. Both host addresses are
	// published.
	AFDual
)

Address families. The netstack IPv6 path has no vehicle in the field, so the matrix is the only place it can be exercised.

func (AddressFamily) String

func (a AddressFamily) String() string

String returns a short stable token for the address family.

type Agreement

type Agreement int

Agreement says whether our client and stock openvpn reached the same verdict about a config, for the case the cell cannot speak to: both of them failed, and the question is whether they failed at the same thing.

It is keyed on the diag class and never on the stage. The stage is a systematic difference between the two clients rather than a tie-breaker: our client reports AUTH_FAILED as ClassAuth at StagePush, correctly, since OpenVPN sends it in place of PUSH_REPLY after a key-method-2 exchange the server accepted, where ClassifyReferenceLog maps the same rejection to StageAuth, equally correctly for a reader of stock openvpn's log. Comparing on the stage would report a disagreement on every credential rejection and on nothing real. Verdict.String reports both stages all the same, as two positions on one agreed failure.

const (
	// AgreementSplit is one of them connecting and the other not. The cell
	// already says everything there is to say; there is no pair of failures
	// to compare, and both CellOurGap and CellSurprising are this.
	AgreementSplit Agreement = iota
	// AgreementBothConnected is both connecting.
	AgreementBothConnected
	// AgreementSameClass is both failing in the same class. The stages may
	// differ, and for a credential rejection they always do.
	AgreementSameClass
	// AgreementDifferentClass is both failing, in different classes. It is the
	// only value that is a real disagreement: the two clients agree the config
	// does not work and give different reasons, so at most one can be right.
	AgreementDifferentClass
)

How far the two clients agree.

func (Agreement) Agrees

func (a Agreement) Agrees() bool

Agrees reports whether the two clients reached the same verdict: both connected, or both failed in the same class. A split is not a disagreement about the reason — it is the cell doing its job — so it is false here.

func (Agreement) String

func (a Agreement) String() string

String returns the agreement's stable token, or an "agreement(N)" placeholder when the value is outside the defined range.

type Auth

type Auth int

Auth is how the server makes the client prove who it is. It is the axis the `auth-user-pass` isolate turns on.

const (
	// AuthCert is certificate-only: the client certificate is the whole
	// credential.
	AuthCert Auth = iota
	// AuthUserPass additionally demands a username and password, which the
	// server checks with an --auth-user-pass-verify hook. The directive's bare
	// form — no file argument — is what the matrix reproduces.
	AuthUserPass
)

Authentication methods. Both keep the client certificate: a deployment that asks for a username and password asks for it *in addition to* a certificate, never instead of one, so there is no cert-less value here.

func (Auth) RequiresCredentials

func (a Auth) RequiresCredentials() bool

RequiresCredentials reports whether the method needs a username and password on top of the client certificate.

func (Auth) String

func (a Auth) String() string

String returns a short stable token for the authentication method.

type AuthPacketInfo

type AuthPacketInfo struct {
	TotalBytes     int    `json:"total_bytes"`
	Options        string `json:"options"`
	Username       string `json:"username"`
	PasswordLen    int    `json:"password_len"`
	CredentialKind string `json:"credential_kind"`
	PeerInfo       string `json:"peer_info"`
}

AuthPacketInfo holds the non-secret metadata logged by the mock server in an "auth_packet_recv" event. Credential values and prefixes are never logged.

type CASource

type CASource int

CASource is where the client profile's certificate authority comes from: an inline block, or a file beside the profile. It is an axis because it changes what a client has to be able to do rather than what goes on the wire, and a profile naming its CA by path has no inline block to fall back on.

const (
	// CAInline is an inline <ca> block, which is what every other matrix
	// entry ships.
	CAInline CASource = iota
	// CAFile is a `ca <path>` reference to a file beside the profile. The
	// path is supplied per run, because it differs between a profile written
	// into a directory on this host and one unpacked into a container.
	CAFile
)

Where the CA comes from.

func (CASource) String

func (s CASource) String() string

String returns a short stable token for the CA source.

type Cell

type Cell int

Cell is one square of the comparison table: what it means that our client and stock openvpn each did what they did.

const (
	// CellWorkingAsIntended is both connecting. Record the features
	// exercised; there is nothing to fix.
	CellWorkingAsIntended Cell = iota
	// CellOurGap is we fail and stock openvpn connects. This is the only
	// cell that generates work: the config and the endpoint are both fine,
	// so the difference is ours.
	CellOurGap
	// CellBadConfigOrDeadEndpoint is both failing. Exclude it from the
	// denominator; it is not a defect in this library.
	CellBadConfigOrDeadEndpoint
	// CellSurprising is we connect and stock openvpn does not. Investigate:
	// most often the reference binary is a different build from the one the
	// config was written for, which is why OracleResult records both.
	CellSurprising
)

The four cells. Exactly one of them generates work.

func (Cell) Action

func (c Cell) Action() string

Action returns what to do about the cell.

func (Cell) GeneratesWork

func (c Cell) GeneratesWork() bool

GeneratesWork reports whether the cell is the one that produces a task, which is CellOurGap and only CellOurGap.

func (Cell) String

func (c Cell) String() string

String returns the cell's stable token, or a "cell(N)" placeholder when the value is outside the defined range.

type CertCheck

type CertCheck int

CertCheck is the additional check the client profile asks for on the server certificate, on top of the chain verification both OpenSSL and Go perform unprompted. It is the axis the certificate-verification isolates turn on.

The two `verify-x509-name` values are one axis apart: the same server and the same certificate, with one profile naming the CN the server has and the other naming one it does not. A client that ignores the directive connects to both, and only the pair can say so.

const (
	// CertCheckNone asks for nothing beyond `remote-cert-tls server`, which
	// every entry carries.
	CertCheckNone CertCheck = iota
	// CertCheckX509NameMatch is `verify-x509-name <CN> name` naming the
	// server certificate's real common name. It must connect.
	CertCheckX509NameMatch
	// CertCheckX509NameMismatch is the same directive naming a common name
	// the server does not have. It must be refused, and the refusal is the
	// result: this entry exists to be failed.
	CertCheckX509NameMismatch
	// CertCheckNSCertType is `ns-cert-type server`, the legacy Netscape
	// certificate-type extension, which nothing in the field asks for — so
	// this is the only place it can be exercised at all. The server
	// certificate is issued with the extension when an entry selects this
	// value and without it otherwise.
	CertCheckNSCertType
)

Certificate checks. All four are client-side; only CertCheckNSCertType also changes the server certificate, because the extension it tests has to be there for the check to mean anything.

func (CertCheck) String

func (c CertCheck) String() string

String returns a short stable token for the certificate check.

type Cipher

type Cipher string

Cipher is a data-channel cipher.

const (
	// CipherAES128CBC is AES-128-CBC.
	CipherAES128CBC Cipher = "AES-128-CBC"
	// CipherAES256CBC is AES-256-CBC.
	CipherAES256CBC Cipher = "AES-256-CBC"
	// CipherAES128GCM is AES-128-GCM.
	CipherAES128GCM Cipher = "AES-128-GCM"
	// CipherAES256GCM is AES-256-GCM.
	CipherAES256GCM Cipher = "AES-256-GCM"
)

Data-channel ciphers in the matrix. AES-128-CBC and AES-256-CBC exercise the two key lengths of the MAC-then-encrypt path; AES-128-GCM and AES-256-GCM do the same for AEAD.

func (Cipher) AEAD

func (c Cipher) AEAD() bool

AEAD reports whether the cipher is an AEAD mode, in which case the digest applies only to the control channel.

type Classification

type Classification struct {
	// Connected reports whether the client logged
	// "Initialization Sequence Completed".
	Connected bool
	// Class is the diag class the failure falls into.
	Class diag.Class
	// Stage is the furthest connection stage the log proves was reached.
	Stage diag.Stage
	// Rule names the classification rule that fired, for example
	// "auth-rejected". It is stable and safe to aggregate on.
	Rule string
	// Evidence is the log line the rule keyed on, with the leading
	// timestamp stripped. Empty when no rule matched.
	Evidence string
	// Detail explains the verdict in one sentence, including what else could
	// produce the same evidence.
	Detail string
	// Ambiguous marks a verdict that more than one root cause produces. It is
	// not a confidence score: the log genuinely does not separate the
	// possibilities named in Detail, and a human reading it could not either.
	Ambiguous bool
}

Classification is what a stock OpenVPN client's log says happened, expressed in the diag vocabulary. It is derived purely from text, so it can be produced and tested without Docker.

When Connected is true the remaining failure fields are meaningless — Class in particular, whose zero value is a real class. This mirrors diag.Outcome, where Class is only defined once Succeeded is false.

func ClassifyReferenceLog

func ClassifyReferenceLog(log string, timedOut bool) Classification

ClassifyReferenceLog turns a stock OpenVPN client's log into a diag verdict.

timedOut says whether the oracle's own deadline expired with the client still running; it only matters when no rule matched, in which case the run is reported as a network timeout at whatever stage the log proves was reached.

It is pure text processing with no Docker dependency, which is what lets the rules be tested against captured logs in the ordinary unit suite.

type ClientOutcome

type ClientOutcome struct {
	// Connected reports whether our client reached a working tunnel.
	Connected bool
	// Class is the diag class our attempt ended with, meaningless when
	// Connected is true.
	Class diag.Class
	// Stage is the furthest stage our attempt reached.
	Stage diag.Stage
}

ClientOutcome is our own client's side of the comparison, reduced to the three things the four-cell table needs.

It is a plain struct rather than a *diag.SessionReport so that testenv does not have to import the root package, which it cannot: the root package's own integration tests import testenv. Build one with ClientOutcomeFromReport.

func ClientOutcomeFromReport

func ClientOutcomeFromReport(r *diag.SessionReport) ClientOutcome

ClientOutcomeFromReport reduces a diag.SessionReport to the comparison inputs. A nil report yields the zero outcome, which reads as "did not connect": a run that produced no report certainly produced no tunnel.

type ClientProfileOptions

type ClientProfileOptions struct {
	// Remote is the host to dial, e.g. "127.0.0.1" or "::1".
	Remote string
	// Port is the published host-side port.
	Port int
	// CACertPEM is the run's CA certificate.
	CACertPEM string
	// ClientCertPEM is the run's client certificate.
	ClientCertPEM string
	// ClientKeyPEM is the run's client private key.
	ClientKeyPEM string
	// StaticKey is the OpenVPN static key file contents, required when the
	// entry's Wrap is not WrapPlain.
	StaticKey string
	// CredentialsFile, when non-empty, is the in-container path the generated
	// auth-user-pass directive points at. Ignored unless the entry's Auth is
	// AuthUserPass. Leave it empty for the bare directive, which is what this
	// client's credential seam consumes; set it for a stock OpenVPN client,
	// which has no terminal to be prompted on inside a container and must read
	// the pair from a file.
	CredentialsFile string

	// CAFile is the path the generated `ca` directive names. Ignored unless
	// the entry's CASource is CAFile, and required when it is.
	//
	// It is per-run because one entry produces two profiles: a bare base name
	// for a profile written into a directory on this host, where OpenVPN and
	// this client both resolve it beside the profile, and an absolute
	// in-container path for the reference client, whose working directory is /
	// and not /etc/openvpn.
	CAFile string

	// DeadRemotePort is the port of the deliberately unreachable first
	// remote. Ignored unless the entry's Remotes is RemoteDeadFirst, and
	// required when it is. Nothing must be listening on it: the point of the
	// entry is that the first remote does not answer, and a port that happens
	// to be in use turns the failover test into an accident.
	DeadRemotePort int
}

ClientProfileOptions carries the per-run values a client profile needs that the matrix entry itself cannot know: where the server ended up listening and the ephemeral PKI generated for that run.

type Compression

type Compression int

Compression is the compression framing mode.

const (
	// CompNone disables compression framing entirely.
	CompNone Compression = iota
	// CompLZO is the legacy comp-lzo framing.
	CompLZO
	// CompStub is the "compress" stub framing (stub-v2 on 2.5 and 2.6).
	CompStub
	// CompLZOForced is `comp-lzo yes`: LZO with adaptive compression turned
	// off, so the peer compresses every payload that comes out smaller
	// instead of only the ones its heuristic judges worth trying.
	//
	// It is the only mode in the matrix that puts a genuinely compressed
	// payload on the wire — every other compression entry negotiates a framing
	// byte and sends plaintext behind it — so it is the only vehicle for the
	// client's LZO decompression.
	//
	// It only works on 2.4: the captures in docker/COMPRESSION-VECTORS.md
	// measured all three pinned builds, and 2.5 and 2.6 servers never compress
	// on send without `allow-compression yes`, so an entry on either connects
	// and tests nothing. TestForcedLZOIsOnAVersionThatCompresses refuses one.
	CompLZOForced
)

Compression modes. Even the stub modes matter: they change the framing byte the data channel must account for.

func (Compression) String

func (c Compression) String() string

String returns a short stable token for the compression mode.

func (Compression) UsesLZO

func (c Compression) UsesLZO() bool

UsesLZO reports whether the mode drives OpenVPN's LZO path, which 2.6 refuses to enable at all unless compression is explicitly re-allowed.

type Config

type Config struct {
	// CRV1Mode, when true, starts the server in AUTH_FAILED,CRV1 mode.
	CRV1Mode bool

	// RedirectGateway, when true, instructs the mock server to include
	// "redirect-gateway def1" in its PUSH_REPLY, simulating AWS Client VPN
	// full-tunnel mode. Use this to test issue #7 bypass-route behaviour.
	RedirectGateway bool

	// Binary is the path to the mock-server binary. The MOCK_SERVER_BIN
	// environment variable is used as a fallback when it is empty.
	Binary string

	// CertDir is the directory containing ca.crt, server.crt, server.key.
	// Default: resolved from the binary's location.
	CertDir string

	// TCPPort is the host-side TCP port. 0 means pick a free port.
	TCPPort int

	// KeepaliveMS overrides how often the server pushes a data-channel
	// keepalive once the tunnel is up, in milliseconds. 0 leaves the mock
	// server's own default, which is already far faster than a real server's
	// pushed ping. Lower it for a test that needs traffic in flight.
	KeepaliveMS int

	// NoKeepalive switches the server's data-channel keepalives off, for a
	// test that wants a silent peer. It takes precedence over KeepaliveMS.
	NoKeepalive bool

	// UDPPort is the host-side UDP port. 0 means pick a free port. A listener
	// is always started, because the mock server starts one unconditionally
	// and a fixed default port would collide between concurrent tests; reach
	// it through Server.UDPAddr.
	UDPPort int
}

Config controls how the mock server is started.

type Digest

type Digest string

Digest is an HMAC digest, used for the data channel in CBC mode and for the control-channel wrapping in every mode.

const (
	// DigestSHA1 is SHA1 (20-byte HMAC).
	DigestSHA1 Digest = "SHA1"
	// DigestSHA256 is SHA256 (32-byte HMAC).
	DigestSHA256 Digest = "SHA256"
	// DigestSHA512 is SHA512 (64-byte HMAC).
	DigestSHA512 Digest = "SHA512"
)

HMAC digests in the matrix: 20-, 32- and 64-byte outputs.

type MatrixEntry

type MatrixEntry struct {
	// Name is a stable identifier, usable both as a Go subtest name and as a
	// lookup key for Entry. It never changes for a given combination.
	Name string

	// Version selects which pinned server image runs the entry.
	Version ServerVersion
	// Cipher is the data-channel cipher.
	Cipher Cipher
	// Digest is the HMAC digest.
	Digest Digest
	// Wrap is the control-channel wrapping mode.
	Wrap Wrap
	// Proto is the transport protocol.
	Proto Proto
	// Compression is the compression framing mode.
	Compression Compression
	// Reneg is the renegotiation policy.
	Reneg Reneg
	// Family is the transport address family.
	Family AddressFamily
	// Auth is how the client authenticates: certificate only, or certificate
	// plus a username and password.
	Auth Auth
	// CertCheck is the additional server-certificate check the client
	// profile asks for.
	CertCheck CertCheck
	// CASource is whether the client profile inlines its CA or names a file.
	CASource CASource
	// Remotes is how many remote lines the client profile carries, and
	// whether the first one answers.
	Remotes RemoteSet

	// DataV1 marks an entry whose purpose is to exercise the P_DATA_V1 data
	// channel (no peer-id prefix).
	//
	// There is no server configuration for it: 2.4, 2.5 and 2.6 all decide the
	// format from the client's IV_PROTO, so the flag is an instruction to the
	// client under test and the rig side is the ordinary server. The
	// instruction is "advertise no IV_PROTO extensions" rather than "omit the
	// DATA_V2 bit", because 2.4 and 2.5 push a peer-id whenever
	// sscanf("IV_PROTO=%d") >= 2 (push.c:prepare_push_reply) where 2.6 tests
	// the bit (multi.c:multi_client_connect_late_setup via extract_iv_proto).
	// vpn.WithholdDataV2 is that value, and a test that does not apply it for
	// an entry with this flag set measures nothing.
	DataV1 bool

	// Unimplemented, when non-empty, explains why the rig cannot bring this
	// entry up. Enumerating tests must skip such entries with this message
	// rather than fail. StartMatrix returns ErrUnimplemented for them.
	Unimplemented string

	// ClientUnsupported, when non-empty, explains why *this repository's*
	// client cannot drive the entry. The rig can: the server starts, the
	// generated profile is correct, and the reference client connects to it.
	//
	// It is not Unimplemented, and the two must never be conflated.
	// StartMatrix refuses an Unimplemented entry, so marking one that way to
	// spare our client would also deny the reference client the chance to
	// prove it. A test driving our client should skip on a non-empty value and
	// say so; one driving the rig or the reference client must ignore it.
	ClientUnsupported string

	// ReferenceRejects, when non-empty, names the ClassifyReferenceLog rule
	// the reference client is expected to report against this entry instead
	// of connecting.
	//
	// Unimplemented and ClientUnsupported both say something could not be
	// tried; this says the entry was tried and the design was to be refused.
	// An entry whose point is a check that fails is only sound if the refusal
	// is the intended one, so the rule is asserted rather than the weaker "did
	// not connect", which a server that never started would also satisfy.
	ReferenceRejects string
}

MatrixEntry is one point in the server matrix. It is the single source for both the server configuration and the client .ovpn profile, so the two cannot drift apart.

func Entry

func Entry(name string) (MatrixEntry, bool)

Entry looks up a matrix entry by name. The boolean reports whether it exists.

func Matrix

func Matrix() []MatrixEntry

Matrix returns the curated matrix entry set. The returned slice is a copy; mutating it does not affect the package-level table.

func MatrixFor

func MatrixFor(v ServerVersion) []MatrixEntry

MatrixFor returns the curated entries for a single server version.

func (MatrixEntry) ClientProfile

func (e MatrixEntry) ClientProfile(o ClientProfileOptions) string

ClientProfile returns a complete inline .ovpn profile for this entry, generated from the same MatrixEntry as ServerConfig so the two agree on cipher, digest, control-channel wrapping, compression framing and renegotiation by construction.

func (MatrixEntry) ServerConfig

func (e MatrixEntry) ServerConfig() string

ServerConfig returns the complete openvpn server configuration for this entry, referencing the PKI paths the entrypoint unpacks into /etc/openvpn.

func (MatrixEntry) SkipReason

func (e MatrixEntry) SkipReason() string

SkipReason returns a non-empty explanation when the entry cannot be started, and the empty string when it can. It covers both the static Unimplemented field and combinations that are invalid for the selected server version.

func (MatrixEntry) String

func (e MatrixEntry) String() string

String returns the entry's name, so an entry can be logged directly.

type MatrixPKI

type MatrixPKI struct {
	// CACertPEM is the throwaway CA certificate.
	CACertPEM string
	// ServerCertPEM is the server certificate, signed by the CA.
	ServerCertPEM string
	// ServerKeyPEM is the server private key.
	ServerKeyPEM string
	// ClientCertPEM is a client certificate, signed by the CA.
	ClientCertPEM string
	// ClientKeyPEM is the client private key.
	ClientKeyPEM string
	// StaticKey is an OpenVPN static key V1 file, used for tls-auth and
	// tls-crypt. Empty when the entry needs no static key.
	StaticKey string
}

MatrixPKI is the ephemeral certificate material generated for one matrix server run. It is created in memory by StartMatrix, handed to the container through an environment variable, and discarded when the process exits: nothing is written to the repository and nothing is reused between runs, which is what keeps credential material out of the tree.

type MatrixServer

type MatrixServer struct {
	// Entry is the matrix entry this server was started from.
	Entry MatrixEntry
	// Image is the exact image tag that was run.
	Image string
	// ContainerID is the Docker container ID.
	ContainerID string
	// Port is the host-side published port, the same for both address
	// families when both are published.
	Port int
	// Addr is the dialable IPv4 host address ("127.0.0.1:port"), empty for
	// an IPv6-only entry.
	Addr string
	// Addr6 is the dialable IPv6 host address ("[::1]:port"), empty for an
	// IPv4-only entry.
	Addr6 string
	// DeadPort is a host port nothing is listening on, published by nothing
	// and reserved for this run. It is the first remote of a RemoteDeadFirst
	// entry's host-side profile, and zero for every other entry.
	DeadPort int
	// PKI is the ephemeral certificate material this server was given.
	PKI MatrixPKI
	// contains filtered or unexported fields
}

MatrixServer is a running OpenVPN server from the matrix.

func StartMatrix

func StartMatrix(e MatrixEntry) (*MatrixServer, error)

StartMatrix brings up the OpenVPN server described by a matrix entry and waits until it logs "Initialization Sequence Completed". It mirrors Start/Stop for the mock server: the caller is responsible for calling Stop, and certificate material is generated fresh for every call.

The returned error wraps ErrUnimplemented for an entry the rig cannot bring up (skip it) and ErrImageMissing when the pinned image has not been built (run `make matrix-images`).

func (*MatrixServer) CheckWithReferenceClient

func (s *MatrixServer) CheckWithReferenceClient(timeout time.Duration) (ReferenceClientResult, error)

CheckWithReferenceClient runs a stock OpenVPN client — the same pinned version, from the same image — against this server in a throwaway container, and reports whether it connected.

This is the matrix's self-check: it asserts on the rig rather than on our client, proving that ServerConfig and ClientProfile, generated from one MatrixEntry, agree on the wire. RunOracle is the oracle proper, sharing the container machinery below but judging an arbitrary config. An error wrapping ErrUnimplemented means the self-check cannot cover this entry; callers should skip on that rather than fail.

func (*MatrixServer) ClientProfile

func (s *MatrixServer) ClientProfile() string

ClientProfile returns the .ovpn profile matching this running server, generated from the same MatrixEntry as its server configuration.

The profile is for a client running on the host — ours. An AuthUserPass entry therefore gets the bare auth-user-pass directive with no file behind it, because the credentials reach our client through its API rather than its profile; a containerised stock client needs the file form and gets it from ContainerProfile.

A CAFile entry's profile names its CA by bare base name, which resolves beside the profile. Use WriteClientProfile to put both on disk together; this method returns the text alone and cannot create the file it references.

func (*MatrixServer) ClientProfileSideFiles

func (s *MatrixServer) ClientProfileSideFiles() map[string]string

ClientProfileSideFiles returns the files a host-side profile from ClientProfile references, keyed by the base name the profile names them under. Only a CAFile entry has any; every other profile is self-contained.

func (*MatrixServer) ContainerIP

func (s *MatrixServer) ContainerIP() (string, error)

ContainerIP returns the server container's address on Docker's default bridge network. It is how a second container reaches the server without going through a published host port.

func (*MatrixServer) ContainerIP6

func (s *MatrixServer) ContainerIP6() (string, error)

ContainerIP6 returns the server container's global IPv6 address. It is only populated for entries that were given a throwaway IPv6 network, i.e. those whose Family is not AFInet.

func (*MatrixServer) ContainerProfile

func (s *MatrixServer) ContainerProfile() (string, error)

ContainerProfile returns the .ovpn profile a sibling container should use to reach this server: the one generated from this server's matrix entry, but pointed at the container's own address on the Docker network rather than at the published host port, which no other container can reach. It returns an error wrapping ErrUnimplemented for an IPv6 entry whose throwaway IPv6 network could not be created; callers should skip on that rather than fail.

func (*MatrixServer) DialAddr

func (s *MatrixServer) DialAddr() string

DialAddr returns the address a client should dial: the IPv4 address when the entry publishes one, otherwise the IPv6 address.

func (*MatrixServer) Interrupt

func (s *MatrixServer) Interrupt() error

Interrupt removes the running container and keeps everything that made it addressable — the published port, the certificate material and the exact config bundle all stay with the MatrixServer, so Resume can put an identical server back at the same address.

Between the two calls nothing is listening on Port, which is what a broken link looks like from the client's side. The session goes with the container, so what comes back is a server that has never heard of this client — the honest version of an endpoint that restarted.

The throwaway IPv6 network, for an entry that needed one, is left in place and Resume reattaches to it: recreating it would renumber the container and change the address the test asserts stays the same.

Interrupt, Resume and Restart are not safe to call concurrently with each other or with Logs, ContainerIP, ContainerProfile and the rest, all of which address the container by the id these replace.

func (*MatrixServer) Logs

func (s *MatrixServer) Logs() (string, error)

Logs returns the container's combined stdout and stderr.

func (*MatrixServer) OracleOptions

func (s *MatrixServer) OracleOptions() OracleOptions

OracleOptions returns the options that point a RunOracle call at this server: the same pinned OpenVPN build the server runs, and the same Docker network, so the reference client can reach it.

Pair it with ContainerProfile:

profile, err := srv.ContainerProfile()
res, err := testenv.RunOracle(ctx, profile, srv.OracleOptions())

func (*MatrixServer) Restart

func (s *MatrixServer) Restart() error

Restart is Interrupt followed by Resume: by the time it returns the server is answering again at the address it was.

A caller that needs the link to stay down for a measurable while calls the two halves itself and chooses the gap.

func (*MatrixServer) Resume

func (s *MatrixServer) Resume() error

Resume brings a replacement container up from the same image, the same config bundle and the same certificate material, published on the same host port, and waits until it logs "Initialization Sequence Completed".

It is a new container rather than a `docker start`, because Interrupt removed the old one. What the client has to find again is the address, which is the published port; nothing else about the container needs to survive.

func (*MatrixServer) Stop

func (s *MatrixServer) Stop() error

Stop removes the container and any throwaway network created for it. It is safe to call more than once.

func (*MatrixServer) WriteClientProfile

func (s *MatrixServer) WriteClientProfile(dir string) (string, error)

WriteClientProfile writes the host-side client profile and every file it references into dir, and returns the path of the profile.

A profile that names a file is only usable as a pair, and a caller reconstructing that pair by hand can get the file name wrong — which shows up as our client refusing a profile the rig generated correctly.

dir is not created and is not cleaned up; pass t.TempDir().

type MockPKI

type MockPKI struct {
	// Dir holds ca.crt, server.crt and server.key. Pass it as Config.CertDir.
	Dir string
	// CAPEM is the CA certificate, for the client profile's CA field.
	CAPEM []byte
}

MockPKI is a throwaway certificate authority for the mock server, written to disk in the layout Config.CertDir expects.

A profile with no CA is a diag.ClassConfig error rather than a silently unverified tunnel, so the mock-server tests are given a CA they can hand to the client and exercise the real certificate-verification path with.

func NewMockPKI

func NewMockPKI(dir string) (*MockPKI, error)

NewMockPKI generates a CA and a server key pair for the mock server and writes them into dir, which must already exist — t.TempDir() is the intended argument.

The server certificate carries the serverAuth extended key usage and loopback IP SANs, matching what the matrix rig issues, so a profile may carry remote-cert-tls server without the certificate being the reason a test fails.

type MockServerEvent

type MockServerEvent struct {
	TS     int64  `json:"ts"`
	Event  string `json:"event"`
	Detail string `json:"detail"`
}

MockServerEvent is one structured log line emitted by the mock server.

type OracleOptions

type OracleOptions struct {
	// Version selects which pinned OpenVPN build runs the config. It names a
	// matrix image, so the exact upstream release is reproducible. Empty
	// means DefaultOracleVersion.
	Version ServerVersion

	// Timeout bounds the whole run. Zero means DefaultOracleTimeout. On
	// expiry the run is reported with TimedOut set rather than as an error.
	Timeout time.Duration

	// HandshakeWindow becomes openvpn's --hand-window. Zero means
	// DefaultOracleHandshakeWindow. Shortening it only makes a stalled
	// handshake fail sooner; it does not change the class.
	HandshakeWindow time.Duration

	// Network is the Docker network to attach the client container to. Empty
	// means the default bridge; use MatrixServer.OracleOptions to reach a
	// matrix server that sits on a throwaway network.
	Network string

	// Files are extra files placed beside the config inside the container,
	// keyed by base name. The config references them as /etc/openvpn/<name>;
	// a name containing a path separator, and "client.conf", are rejected.
	Files map[string]string

	// Directives are appended after the oracle preamble, so they win over both
	// the config and the preamble. Use sparingly: each one is a way the thing
	// being judged differs from the thing that was handed in.
	Directives []string
}

OracleOptions configures a reference-oracle run. The zero value is usable and selects DefaultOracleVersion, DefaultOracleTimeout and DefaultOracleHandshakeWindow.

func (OracleOptions) Preamble

func (o OracleOptions) Preamble() []string

Preamble returns the directives the oracle appends to every config it runs. They bound the run without changing its outcome: stock openvpn retries forever by default, so without them a dead endpoint costs the full timeout and a config error costs nothing but looks the same.

verb 3             a floor and a ceiling on log detail, so the classifier
                   sees the same lines whatever the config asked for
tls-exit           exit on TLS failure instead of restarting the attempt
connect-retry-max  try each remote once
hand-window        how long to wait for the control channel
resolv-retry 0     resolve once; do not spin on a dead name
cd                 resolve a relative file reference beside the config

The applied list is reported in OracleResult.Preamble, so a verdict can always be reproduced by hand.

The last is the exception: it does not bound the run. OpenVPN resolves a relative "ca ca.example.crt" against the process's working directory rather than the directory the config was read from, and the container starts in "/". Files stages material beside the config, so without this the reference client fails to open a CA sitting next to it and the oracle judges its own staging instead of the profile.

type OracleResult

type OracleResult struct {
	// Classification is the verdict derived from the client's log.
	Classification

	// Image is the exact image tag that ran, for example
	// "go-openvpn-test/openvpn-server:2.6.22".
	Image string
	// Banner is the openvpn version banner from the run, for example
	// "OpenVPN 2.6.22 x86_64-pc-linux-gnu [SSL (OpenSSL)] ... built on ...".
	// This is the authoritative record of which binary produced the verdict.
	Banner string
	// Release is the exact upstream release parsed out of Banner, for
	// example "2.6.22".
	Release string
	// HostBanner is the host's own `openvpn --version` first line, or empty
	// when the host has no openvpn. The oracle never runs it: it is recorded
	// for contrast, the usual explanation for "we connect, openvpn does not".
	HostBanner string

	// Preamble is the directive list the oracle appended to the config.
	Preamble []string
	// Elapsed is how long the client ran.
	Elapsed time.Duration
	// TimedOut reports that the oracle's own Timeout expired with the client
	// still running. The classification is then whatever the partial log
	// supports.
	TimedOut bool
	// Log is the client container's complete log.
	Log string
}

OracleResult is one reference-oracle run: what stock openvpn did with the config, and which openvpn did it.

func RunOracle

func RunOracle(ctx context.Context, config string, opts OracleOptions) (OracleResult, error)

RunOracle runs stock openvpn against config in a throwaway container and classifies what it did.

config is a complete .ovpn profile, used as given except for the appended preamble (see OracleOptions.Preamble), which bounds the run without changing its outcome and is reported in the result.

A failure to connect is not an error: it is the result. An error means the oracle itself could not run — no Docker, no image, a bad Files entry — and the returned result is then not meaningful. The error wraps ErrImageMissing when the pinned image has not been built and ErrUnimplemented when no image is pinned for the requested version.

func (OracleResult) String

func (r OracleResult) String() string

String renders the result as one line, always naming the openvpn that ran.

type Proto

type Proto int

Proto is the transport protocol.

const (
	// ProtoUDP is UDP transport.
	ProtoUDP Proto = iota
	// ProtoTCP is TCP transport, which adds the 2-byte length prefix.
	ProtoTCP
)

Transport protocols. UDP and TCP differ in framing and in who provides reliability.

func (Proto) String

func (p Proto) String() string

String returns "udp" or "tcp".

type ReferenceClientResult

type ReferenceClientResult struct {
	// Connected reports whether the reference client completed its
	// initialization sequence, i.e. handshake, auth and push all succeeded.
	Connected bool
	// Log is the reference client's container log.
	Log string
	// Classification is the same verdict the reference oracle would reach
	// from this log, so an entry that is meant to be refused can be checked
	// against the reason it was refused rather than merely against the fact.
	// "Did not connect" is also what a server that failed to start produces.
	Classification Classification
}

ReferenceClientResult is the outcome of running a stock OpenVPN client of the same pinned version against a matrix server.

type RemoteSet

type RemoteSet int

RemoteSet is how many `remote` lines the client profile carries, and whether the first of them is alive. It is the axis the failover isolate turns on.

const (
	// RemoteSingle is one remote, the running server. Every entry that does
	// not say otherwise is this.
	RemoteSingle RemoteSet = iota
	// RemoteDeadFirst is two remotes: a port nothing is listening on,
	// followed by the running server. A client that tries only the first
	// never connects; one that fails over connects on the second and can say
	// why the first did not answer.
	RemoteDeadFirst
)

Remote sets.

func (RemoteSet) String

func (r RemoteSet) String() string

String returns a short stable token for the remote set.

type Reneg

type Reneg int

Reneg is the renegotiation policy.

const (
	// RenegDefault leaves both ends at the OpenVPN default (3600s).
	RenegDefault Reneg = iota
	// RenegSec30 sets reneg-sec 30 on both ends, so a rekey happens inside a
	// test's lifetime.
	RenegSec30
	// RenegServerInitiated sets reneg-sec 30 on the server and reneg-sec 0 on
	// the client, so only the server can start a rekey.
	RenegServerInitiated
)

Renegotiation policies. A short reneg interval cannot be observed against a live deployment, which is why the matrix has to carry one.

func (Reneg) String

func (r Reneg) String() string

String returns a short stable token for the renegotiation policy.

type Server

type Server struct {
	TCPAddr string // "127.0.0.1:<port>"
	UDPAddr string // "127.0.0.1:<port>"
	// contains filtered or unexported fields
}

Server represents a running mock server.

func Start

func Start(cfg Config) (*Server, error)

Start launches the mock server and waits until it is ready.

func (*Server) AuthEvents

func (s *Server) AuthEvents() []AuthPacketInfo

AuthEvents returns all "auth_packet_recv" events logged by the server, parsed into AuthPacketInfo structs for easy assertion in tests.

func (*Server) EventLog

func (s *Server) EventLog() []MockServerEvent

EventLog returns a snapshot of the events logged by the server so far.

func (*Server) Stop

func (s *Server) Stop() error

Stop terminates the mock server.

func (*Server) WaitForEvents

func (s *Server) WaitForEvents(name string, n int, within time.Duration) int

WaitForEvents blocks until the server's log holds at least n events named name, and returns how many it saw. It gives up after within and returns the count it reached, so a caller can assert on a shortfall rather than on a timeout.

EventLog is the far end of an asynchronous pipeline — stdout line, OS pipe, scanner, buffered channel, appending goroutine — so a test that reads it on the line after acting on the client is racing all of that, and the symptom is a count one short that reads exactly like a lost packet. Keep the window generous: the point is to remove the race, not to bound the latency.

type ServerVersion

type ServerVersion string

ServerVersion names an OpenVPN server minor series covered by the matrix.

const (
	// V24 is OpenVPN 2.4 — no TLS keying-material exporter, so the classic
	// PRF path is the only one available.
	V24 ServerVersion = "2.4"
	// V25 is OpenVPN 2.5.
	V25 ServerVersion = "2.5"
	// V26 is OpenVPN 2.6.
	V26 ServerVersion = "2.6"
)

The three server series in the matrix.

func ServerVersions

func ServerVersions() []ServerVersion

ServerVersions returns every server series in the matrix, in ascending order.

type Verdict

type Verdict struct {
	// Cell is which square of the four-cell table this config landed in.
	Cell Cell
	// Agreement is how far the two outcomes agree. It is keyed on the class
	// alone; see the type's documentation for why the stage is excluded.
	Agreement Agreement
	// Ours is our client's outcome.
	Ours ClientOutcome
	// Reference is the stock openvpn run it was compared against.
	Reference OracleResult
}

Verdict is the oracle's result for one config: what we did, what stock openvpn did, which cell that puts the config in, and how far the two agree.

Rolled up per provider it is also the rot detector: a provider where nothing connects and stock openvpn fails across the board has a stale account or config set and needs a human, not a bug fix.

func Compare

func Compare(ours ClientOutcome, ref OracleResult) Verdict

Compare produces the oracle's verdict from the two outcomes.

func (Verdict) String

func (v Verdict) String() string

String renders the verdict as one line: the cell, what the two clients did, and the openvpn release that arbitrated.

Both stages are always in the line. Where the classes agree they are written as two positions on one failure — "both fail auth — ours at push, stock openvpn at auth" — rather than side by side, which would read as a dispute.

type Wrap

type Wrap int

Wrap is the control-channel wrapping mode. Each value changes the on-the-wire layout of control packets.

const (
	// WrapPlain is an unwrapped control channel: no tls-auth, no tls-crypt.
	WrapPlain Wrap = iota
	// WrapTLSAuthKD0 is tls-auth with the client at key-direction 0 (server 1).
	WrapTLSAuthKD0
	// WrapTLSAuthKD1 is tls-auth with the client at key-direction 1 (server 0).
	// This is the conventional deployment.
	WrapTLSAuthKD1
	// WrapTLSCrypt is tls-crypt: control packets are encrypted as well as
	// authenticated.
	WrapTLSCrypt
)

Control-channel wrapping modes. The KD0/KD1 suffix is the *client's* key-direction, matching how a .ovpn profile reads; the server is given the opposite direction.

func (Wrap) String

func (w Wrap) String() string

String returns a short stable token for the wrapping mode.

func (Wrap) UsesStaticKey

func (w Wrap) UsesStaticKey() bool

UsesStaticKey reports whether the mode needs a shared static key file.

Directories

Path Synopsis
Command mockserver is the go-openvpn mock OpenVPN server.
Command mockserver is the go-openvpn mock OpenVPN server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL