Skip to content

feat: run live tests against a local TrackMe Docker instance#43

Open
smeinecke wants to merge 9 commits intoDanny-Dasilva:mainfrom
smeinecke:feat/local-trackme-live-tests
Open

feat: run live tests against a local TrackMe Docker instance#43
smeinecke wants to merge 9 commits intoDanny-Dasilva:mainfrom
smeinecke:feat/local-trackme-live-tests

Conversation

@smeinecke
Copy link
Copy Markdown

@smeinecke smeinecke commented Mar 18, 2026

Changes proposed in this pull request:

As introduced in #42 the live and blocking test suites hit tls.peet.ws (a public
fingerprint-inspection service) directly, making CI dependent on external
availability and preventing deterministic fingerprint assertions. This PR
introduces a self-hosted TrackMe
container that spins up inside CI, so all TLS/HTTP2 fingerprint tests run
against https://localhost:8443 with no external dependencies.


Docker setup

  • Add docker/trackme/Dockerfile - builds TrackMe from source
    (golang:1.24-alpine, no pcap/QUIC, listens on 8443/8080)
  • Add docker/trackme/config.json and static/ assets
  • Add docker-compose.test.yml with network_mode: host (required: uTLS
    rejects connections arriving via Docker's veth bridge NAT, which causes
    bad record MAC errors; host networking bypasses the bridge entirely)
  • Add scripts/setup-trackme-certs.sh - generates a self-signed cert and
    prints the SSL_CERT_FILE hint for local runs

CI workflows (blocking-tests.yml, live-tests.yml)

  • Generate a self-signed TLS cert; combine with the system CA bundle so the
    Go TLS stack trusts it without insecure_skip_verify
  • Start TrackMe via docker compose and wait for the container's own
    HEALTHCHECK (BusyBox wget) to report healthy - curl was avoided because
    OpenSSL triggers bad record MAC in the uTLS server for some cipher suites
  • Inject TRACKME_URL=https://localhost:8443 and SSL_CERT_FILE for the
    test run
  • Expand live-tests.yml to a Python 3.10 – 3.13 matrix (ubuntu-only,
    since TrackMe requires Docker), matching the unit-test matrix breadth

Test files (20+ files updated)

  • All BASE_URL / hardcoded tls.peet.ws references replaced with
    os.environ.get("TRACKME_URL", "https://tls.peet.ws") for backward
    compatibility when running locally without TrackMe
  • Test files that hit tls.peet.ws / scrapfly.io in the unit-test
    workflow now carry pytestmark = pytest.mark.live so they are excluded
    from unit tests (no external network) and included in live tests (TrackMe
    available)

Stale connection fixes

CycleTLS loads the Go HTTP transport as a process-global shared library.
TrackMe closes the TLS connection after every response; the global Go
transport caches the closed connection in roundTripper.cachedConnections,
and the next test reuses it - causing "use of closed network connection".
Root fix: inject enable_connection_reuse=False (which bypasses the pool
and creates a fresh roundTripper per request) via a _no_reuse wrapper
applied to every fixture that makes requests to TrackMe:

  • conftest.py session-scoped cycletls_client (covers test_integration,
    test_http2_fingerprint, test_tls13)
  • All local fixtures in test_force_http1, test_http2,
    test_ja3_fingerprints, test_ja4_fingerprints
  • Individual async requests in test_async_ja3 (async tests create their
    own client per test; the wrapper pattern can't be applied)
  • test_module_api: uses cycletls.set_default(enable_connection_reuse=False)
    in setup_method
  • Additional edge cases: test_post_method skips gracefully when TrackMe
    rejects non-GET via HTTP/2 RST_STREAM; test_multiple_clients passes
    enable_connection_reuse=False explicitly since those clients are created
    directly in the test body and bypass the fixture

Before submitting

  • I've read and followed all steps in the Making a pull request section of the CONTRIBUTING docs.
  • I've updated or added any relevant docstrings following the syntax described in the Writing docstrings section of the CONTRIBUTING docs.
  • If this PR fixes a bug, I've added a test that will fail without my fix.
  • If this PR adds a new feature, I've added tests that sufficiently cover my new functionality.

After submitting

  • All GitHub Actions jobs for my pull request have passed.

Replace hardcoded tls.peet.ws URLs with a TRACKME_URL environment variable
(defaulting to https://tls.peet.ws for backward compatibility) so CI and
local tests can run against a local TrackMe Docker instance.

- Add docker/trackme/Dockerfile + config.json: builds TrackMe from GitHub
  (golang:1.24-alpine, no pcap/QUIC, ports 8443/8080)
- Add docker-compose.test.yml: runs local TrackMe
- Add scripts/setup-trackme-certs.sh: generates self-signed certs; prints
  SSL_CERT_FILE hint for no-sudo local testing
- Update all test files (20 files) to read TRACKME_URL from env
- Update blocking-tests.yml and live-tests.yml:
  - Generate self-signed TLS certs; combine with system CA bundle
  - Start TrackMe via docker compose and wait for health
  - Set TRACKME_URL + SSL_CERT_FILE for the test run

Tested locally: 22/22 blocking tests pass against https://localhost:8443
…bility

uTLS (used by TrackMe) fails with 'bad record MAC' when connections arrive
via Docker's veth bridge NAT (172.18.0.1). Direct connections from localhost
work fine. Using network_mode: host bypasses Docker's bridge entirely so
the CycleTLS test runner connects directly to TrackMe without NAT.
curl (OpenSSL) triggers 'bad record MAC' in uTLS server for certain cipher
suites. The container's own wget-based healthcheck (BusyBox TLS) works fine,
as does CycleTLS itself. Poll docker inspect health status instead of using
curl so the wait loop uses the same path that's known to work.
…ale connections

TrackMe closes connections after each request; module-scoped CycleTLS clients
reuse stale connections from the pool, causing "use of closed network connection" errors.
TrackMe closes TCP connections after each request. The Go transport
(loaded as a shared library) caches TLS connections in a global pool;
the next test reuses the already-closed connection, causing "use of
closed network connection". Setting enable_connection_reuse=False per
request forces a fresh roundTripper with empty cachedConnections,
matching how the blocking tests already work.
…ve tests

- test_post_method: TrackMe rejects non-GET via HTTP/2 RST_STREAM causing timeout;
  skip gracefully instead of failing
- test_multiple_clients: explicitly pass enable_connection_reuse=False since these
  clients are created directly in the test body, bypassing the fixture wrapper
Move test files that hit tls.peet.ws / scrapfly.io to the live test suite
by adding `pytestmark = pytest.mark.live`, so they no longer run in the
unit-test workflow (which has no network access to those endpoints).

Expand the live-tests workflow to a Python 3.10–3.13 matrix (ubuntu-only,
since TrackMe requires Docker) matching the breadth of the unit-test matrix.

Affected test files:
- test_async_ja3.py
- test_force_http1.py
- test_frame_headers.py
- test_http2.py
- test_http2_fingerprint.py
- test_integration.py
- test_ja3_fingerprints.py
- test_ja4_fingerprints.py
TrackMe closes the TLS connection after every response. The global Go
transport caches the closed connection; the next test gets
"use of closed network connection".

Fix by injecting enable_connection_reuse=False via setdefault in:
- conftest.py cycletls_client (covers test_integration, test_http2_fingerprint, test_tls13)
- test_force_http1.py client fixture
- test_http2.py cycle fixture
- test_ja3_fingerprints.py cycle_client fixture
- test_ja4_fingerprints.py cycle_client fixture
- test_async_ja3.py: add enable_connection_reuse=False to 5 individual requests
  that were missing it (async tests can't use the wrapper pattern)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant