diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8095780..cc18ec6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.26.5 + GO_VERSION: 1.26.6 NODE_VERSION: 24.19.0 BUILDX_METADATA_PROVENANCE: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index bd4dc60..fc275e9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.26.5 + GO_VERSION: 1.26.6 NODE_VERSION: 24.19.0 jobs: @@ -109,7 +109,7 @@ jobs: package-manager-cache: true - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 with: languages: ${{ matrix.language }} @@ -123,4 +123,4 @@ jobs: run: go build ./cmd/multispeed - name: Analyze - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e26cd..e265d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to MultiSpeed are documented here. The project follows [Sema ## [Unreleased] +### Security + +- Upgrade the build toolchain to Go 1.26.6 and the patched LibreSpeed overlay to `golang.org/x/net` v0.56.0. + ## [1.0.4] - 2026-08-11 ### Fixed diff --git a/Dockerfile b/Dockerfile index def61df..6fb6f91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.7 ARG NODE_VERSION=24.19.0 -ARG GO_VERSION=1.26.5 +ARG GO_VERSION=1.26.6 ARG DEBIAN_RELEASE=bookworm FROM node:${NODE_VERSION}-${DEBIAN_RELEASE}-slim@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03 AS frontend-build @@ -21,10 +21,10 @@ RUN npm run build && \ /out/dependency-licenses \ /license-overrides/npm -FROM golang:${GO_VERSION}-${DEBIAN_RELEASE}@sha256:8d36439c36258ba98de1bf2b316eda72905f9d743117119f6db9705c49245644 AS librespeed-build +FROM golang:${GO_VERSION}-${DEBIAN_RELEASE}@sha256:116d58cbd88c1297624acc6e967a060012422bacf9930927e23fb719189c6f36 AS librespeed-build ARG LIBRESPEED_VERSION=v1.0.13 -ARG LIBRESPEED_PATCH_VERSION=multispeed.dns2.xnet055 -ARG LIBRESPEED_X_NET_VERSION=v0.55.0 +ARG LIBRESPEED_PATCH_VERSION=multispeed.dns2.xnet056 +ARG LIBRESPEED_X_NET_VERSION=v0.56.0 ARG BUILD_DATE=unknown ENV CGO_ENABLED=0 \ GOOS=linux \ @@ -78,7 +78,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \ cd /out/source; \ sha256sum "librespeed-cli-${LIBRESPEED_VERSION}-multispeed-corresponding-source.tar.gz" > "librespeed-cli-${LIBRESPEED_VERSION}-multispeed-corresponding-source.tar.gz.sha256" -FROM golang:${GO_VERSION}-${DEBIAN_RELEASE}@sha256:8d36439c36258ba98de1bf2b316eda72905f9d743117119f6db9705c49245644 AS backend-build +FROM golang:${GO_VERSION}-${DEBIAN_RELEASE}@sha256:116d58cbd88c1297624acc6e967a060012422bacf9930927e23fb719189c6f36 AS backend-build ARG VERSION=dev ARG VCS_REF=unknown ARG BUILD_DATE=unknown @@ -113,8 +113,8 @@ ARG VERSION=dev ARG VCS_REF=unknown ARG BUILD_DATE=unknown ARG LIBRESPEED_VERSION=v1.0.13 -ARG LIBRESPEED_PATCH_VERSION=multispeed.dns2.xnet055 -ARG LIBRESPEED_X_NET_VERSION=v0.55.0 +ARG LIBRESPEED_PATCH_VERSION=multispeed.dns2.xnet056 +ARG LIBRESPEED_X_NET_VERSION=v0.56.0 LABEL org.opencontainers.image.title="MultiSpeed" \ org.opencontainers.image.description="Production-ready multi-WAN speed-test monitor" \ diff --git a/README.md b/README.md index dd7fa46..361e60f 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ No environment-variable values are exposed by the system API. ## Development -Prerequisites are Go 1.26.5, Node.js 24, npm 11, and Linux for networking/integration behavior. +Prerequisites are Go 1.26.6, Node.js 24, npm 11, and Linux for networking/integration behavior. ```bash go mod download diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 3b64e31..b54a69b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -12,9 +12,9 @@ MultiSpeed is MIT-licensed. Third-party components remain under their respective - License: GNU Lesser General Public License v3.0 (LGPL-3.0-only) - Relationship: separately built and separately executed, replaceable subprocess; it is not linked into the MultiSpeed binary -The production image builds LibreSpeed CLI from the tagged source and applies the MultiSpeed `multispeed.dns2.xnet055` overlay under LGPL-3.0-or-later. The overlay makes UDP DNS and TCP fallback bind the same selected source address as the HTTP test sockets, pins authorized custom runs to their pre-resolved IP:port endpoints, blocks redirects before follow-up requests, and pins `golang.org/x/net` v0.55.0 in place of upstream v0.49.0. The image and release assets include a deterministic complete corresponding-source archive containing the exact patched tree, vendored dependency source, build script, integration test, module metadata, and the full GPLv3 and LGPLv3 license texts. Telemetry is disabled by default. +The production image builds LibreSpeed CLI from the tagged source and applies the MultiSpeed `multispeed.dns2.xnet056` overlay under LGPL-3.0-or-later. The overlay makes UDP DNS and TCP fallback bind the same selected source address as the HTTP test sockets, pins authorized custom runs to their pre-resolved IP:port endpoints, blocks redirects before follow-up requests, and pins `golang.org/x/net` v0.56.0 in place of upstream v0.49.0. The image and release assets include a deterministic complete corresponding-source archive containing the exact patched tree, vendored dependency source, build script, integration test, module metadata, and the full GPLv3 and LGPLv3 license texts. Telemetry is disabled by default. -LibreSpeed CLI copyright remains with its upstream contributors. It is provided without warranty under LGPL-3.0-only; the MultiSpeed overlay files are offered under LGPL-3.0-or-later. You may replace `/usr/local/bin/librespeed-cli` in a private deployment with a compatible build, subject to the component's license. MultiSpeed fails closed unless that build advertises the `+multispeed.dns2.xnet055` marker and therefore attests to the required source-bound resolver, destination-pinning, and patched dependency baseline. +LibreSpeed CLI copyright remains with its upstream contributors. It is provided without warranty under LGPL-3.0-only; the MultiSpeed overlay files are offered under LGPL-3.0-or-later. You may replace `/usr/local/bin/librespeed-cli` in a private deployment with a compatible build, subject to the component's license. MultiSpeed fails closed unless that build advertises the `+multispeed.dns2.xnet056` marker and therefore attests to the required source-bound resolver, destination-pinning, and patched dependency baseline. ## Optional operator component not distributed by MultiSpeed diff --git a/docs/development.md b/docs/development.md index 8587950..ec5e638 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,7 +4,7 @@ MultiSpeed's networking behavior is Linux-specific. Editing and unit tests can r ## Toolchain -- Go 1.26.5 +- Go 1.26.6 - Node.js 24 and npm 11 - Docker Engine with Compose v2 and BuildKit - `golangci-lint` v2 (version pinned in CI) diff --git a/docs/providers.md b/docs/providers.md index 517edd2..ff2ebea 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -18,7 +18,7 @@ Cloudflare is a trademark and/or registered trademark of Cloudflare, Inc. MultiS The production image builds official `librespeed/speedtest-cli` v1.0.13 from the immutable source tag as a separate, replaceable executable, then applies MultiSpeed's small LGPL source-bound DNS and destination-pinning overlay. Runtime notices, the complete overlay, license texts, and dependency notices are shipped under `/usr/share/doc/librespeed-cli`; the deterministic complete corresponding-source archive and checksum are under `/opt/multispeed/release-artifacts` and attached to each GitHub release. -MultiSpeed uses JSON output, `--source
`, `--no-icmp`, a bounded timeout, and `--telemetry-level disabled`. Upstream v1.0.13 binds HTTP sockets for `--source` but otherwise leaves DNS on the default resolver; the overlay installs a pure-Go resolver whose UDP and TCP connections bind the same source address, pins authorized custom runs to the pre-resolved IP addresses and canonical port, and rejects redirects before a follow-up request. The build replaces upstream's vulnerable `golang.org/x/net` v0.49.0 with pinned v0.55.0. The adapter refuses a replacement CLI unless its version carries the `+multispeed.dns2.xnet055` compatibility marker, preventing an unpatched or dependency-vulnerable executable from silently weakening the release baseline. It supports automatic selection, a public server ID, and deployment-authorized custom server definitions. Certificate verification remains enabled by default; any per-server bypass must be explicit and is recorded in result metadata. +MultiSpeed uses JSON output, `--source
`, `--no-icmp`, a bounded timeout, and `--telemetry-level disabled`. Upstream v1.0.13 binds HTTP sockets for `--source` but otherwise leaves DNS on the default resolver; the overlay installs a pure-Go resolver whose UDP and TCP connections bind the same source address, pins authorized custom runs to the pre-resolved IP addresses and canonical port, and rejects redirects before a follow-up request. The build replaces upstream's vulnerable `golang.org/x/net` v0.49.0 with pinned v0.56.0. The adapter refuses a replacement CLI unless its version carries the `+multispeed.dns2.xnet056` compatibility marker, preventing an unpatched or dependency-vulnerable executable from silently weakening the release baseline. It supports automatic selection, a public server ID, and deployment-authorized custom server definitions. Certificate verification remains enabled by default; any per-server bypass must be explicit and is recorded in result metadata. Custom backend URLs are fail-closed. The deployment must list each complete base URL in the comma-separated `APP_ALLOWED_CUSTOM_SERVER_URLS` environment variable; an empty value authorizes none. Entries are canonicalized once, and a task URL is accepted only when its canonical form equals an allowlist entry. Credentials, queries, fragments, IPv6 zones, ambiguous hosts, encoded or traversal-like paths, and unsafe path characters are rejected. HTTPS is the default. Listing an `http://` URL authorizes the destination but does not bypass transport policy: the individual task must also enable its existing `allowInsecure` option. Keep this list limited to LibreSpeed servers operated or explicitly trusted by the deployment owner. diff --git a/internal/execution/manager_test.go b/internal/execution/manager_test.go index 0d99923..5868f80 100644 --- a/internal/execution/manager_test.go +++ b/internal/execution/manager_test.go @@ -42,7 +42,7 @@ type availabilityOnlyLibreSpeedRunner struct { func (r *availabilityOnlyLibreSpeedRunner) Run(_ context.Context, request providerprocess.Request) (providerprocess.Result, error) { if len(request.Arguments) == 1 && request.Arguments[0] == "--version" { r.versionCalls.Add(1) - return providerprocess.Result{Stdout: "librespeed-cli v1.0.13+multispeed.dns2.xnet055"}, nil + return providerprocess.Result{Stdout: "librespeed-cli v1.0.13+multispeed.dns2.xnet056"}, nil } r.executionCalls.Add(1) return providerprocess.Result{}, errors.New("unexpected LibreSpeed test execution") diff --git a/internal/providers/librespeed/librespeed.go b/internal/providers/librespeed/librespeed.go index a0f373e..5d72cd8 100644 --- a/internal/providers/librespeed/librespeed.go +++ b/internal/providers/librespeed/librespeed.go @@ -29,7 +29,7 @@ type Adapter struct { } const ( - requiredDNSPatchMarker = "+multispeed.dns2.xnet055" + requiredDNSPatchMarker = "+multispeed.dns2.xnet056" allowedServerEndpointsEnvironment = "MULTISPEED_PROVIDER_ALLOWED_SERVER_ENDPOINTS" ) diff --git a/internal/providers/librespeed/librespeed_test.go b/internal/providers/librespeed/librespeed_test.go index 1efdd59..fd78139 100644 --- a/internal/providers/librespeed/librespeed_test.go +++ b/internal/providers/librespeed/librespeed_test.go @@ -20,7 +20,7 @@ func (r *recordingRunner) Run(_ context.Context, request providerprocess.Request if len(request.Arguments) == 1 && request.Arguments[0] == "--version" { version := r.version if version == "" { - version = "librespeed-cli v1.0.13+multispeed.dns2.xnet055" + version = "librespeed-cli v1.0.13+multispeed.dns2.xnet056" } return providerprocess.Result{Stdout: version}, nil } @@ -34,7 +34,7 @@ func TestAvailabilityRejectsUnpatchedCLI(t *testing.T) { } for _, version := range []string{ "librespeed-cli v1.0.13", - "librespeed-cli v1.0.13+multispeed.dns1.xnet055", + "librespeed-cli v1.0.13+multispeed.dns2.xnet055", } { adapter := New(binary, &recordingRunner{version: version}) availability := adapter.Availability(context.Background()) diff --git a/scripts/docker-smoke.ps1 b/scripts/docker-smoke.ps1 index 78e31cb..8cdc9f0 100644 --- a/scripts/docker-smoke.ps1 +++ b/scripts/docker-smoke.ps1 @@ -83,7 +83,7 @@ try { $fixture = @( '#!/bin/sh', 'if [ "${1:-}" = "--version" ]; then', - " printf '%s\n' 'librespeed-cli v1.0.13+multispeed.dns2.xnet055 smoke fixture'", + " printf '%s\n' 'librespeed-cli v1.0.13+multispeed.dns2.xnet056 smoke fixture'", ' exit 0', 'fi', 'printf ''%s\n'' ''[{"timestamp":"2026-01-01T00:00:00Z","server":{"name":"Local smoke fixture","url":"http://127.0.0.1"},"client":{"ip":"203.0.113.10"},"bytes_sent":62500000,"bytes_received":125000000,"ping":8.25,"jitter":0.75,"upload":50,"download":100,"share":""}]''' diff --git a/scripts/docker-smoke.sh b/scripts/docker-smoke.sh index 7192d79..b7c61db 100644 --- a/scripts/docker-smoke.sh +++ b/scripts/docker-smoke.sh @@ -25,7 +25,7 @@ mkdir -p "${data_dir}" cat >"${fake_cli}" <<'EOF' #!/bin/sh if [ "${1:-}" = "--version" ]; then - printf '%s\n' 'librespeed-cli v1.0.13+multispeed.dns2.xnet055 smoke fixture' + printf '%s\n' 'librespeed-cli v1.0.13+multispeed.dns2.xnet056 smoke fixture' exit 0 fi printf '%s\n' '[{"timestamp":"2026-01-01T00:00:00Z","server":{"name":"Local smoke fixture","url":"http://127.0.0.1"},"client":{"ip":"203.0.113.10"},"bytes_sent":62500000,"bytes_received":125000000,"ping":8.25,"jitter":0.75,"upload":50,"download":100,"share":""}]' diff --git a/third_party/librespeed/README.md b/third_party/librespeed/README.md index 800f932..76be0b8 100644 --- a/third_party/librespeed/README.md +++ b/third_party/librespeed/README.md @@ -2,9 +2,9 @@ MultiSpeed builds the immutable `github.com/librespeed/speedtest-cli` v1.0.13 module, then applies this LGPL-3.0-or-later overlay before compiling it. -The build also pins `golang.org/x/net` v0.55.0 (and the compatible transitive -versions selected by Go 1.26.5) instead of upstream's vulnerable v0.49.0. -The resulting compatibility marker is `+multispeed.dns2.xnet055`. +The build also pins `golang.org/x/net` v0.56.0 (and the compatible transitive +versions selected by Go 1.26.6) instead of upstream's vulnerable v0.49.0. +The resulting compatibility marker is `+multispeed.dns2.xnet056`. The upstream `--source` implementation sets `net.Dialer.LocalAddr` for HTTP connections but leaves hostname lookups on `net.DefaultResolver`. MultiSpeed diff --git a/third_party/librespeed/build-corresponding-source.sh b/third_party/librespeed/build-corresponding-source.sh index d32346e..e5a6e53 100644 --- a/third_party/librespeed/build-corresponding-source.sh +++ b/third_party/librespeed/build-corresponding-source.sh @@ -8,7 +8,7 @@ export GOOS=linux export GOARCH=amd64 version="${LIBRESPEED_VERSION:-v1.0.13}" -patch_version="${LIBRESPEED_PATCH_VERSION:-multispeed.dns2.xnet055}" +patch_version="${LIBRESPEED_PATCH_VERSION:-multispeed.dns2.xnet056}" build_date="${BUILD_DATE:-unknown}" output="${OUTPUT:-./librespeed-cli}" diff --git a/web/e2e/Dockerfile b/web/e2e/Dockerfile index a4dfa1c..b213e29 100644 --- a/web/e2e/Dockerfile +++ b/web/e2e/Dockerfile @@ -1,9 +1,9 @@ # syntax=docker/dockerfile:1.7 -ARG GO_VERSION=1.26.5 +ARG GO_VERSION=1.26.6 ARG DEBIAN_RELEASE=bookworm -FROM golang:${GO_VERSION}-${DEBIAN_RELEASE}@sha256:8d36439c36258ba98de1bf2b316eda72905f9d743117119f6db9705c49245644 AS build +FROM golang:${GO_VERSION}-${DEBIAN_RELEASE}@sha256:116d58cbd88c1297624acc6e967a060012422bacf9930927e23fb719189c6f36 AS build WORKDIR /src COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download diff --git a/web/e2e/fixtures/librespeed-cli b/web/e2e/fixtures/librespeed-cli index f7bb39d..0bc879a 100644 --- a/web/e2e/fixtures/librespeed-cli +++ b/web/e2e/fixtures/librespeed-cli @@ -3,7 +3,7 @@ set -eu case " $* " in *" --version "*) - printf '%s\n' 'librespeed-cli v1.0.13+multispeed.dns2.xnet055 deterministic-e2e' + printf '%s\n' 'librespeed-cli v1.0.13+multispeed.dns2.xnet056 deterministic-e2e' ;; *" --list "*) printf '%s\n' '201: Berlin Fixture (https://librespeed.fixture.invalid) [Sponsor: MultiSpeed E2E @ https://github.com/dude2k/MultiSpeed]' diff --git a/web/e2e/multispeed.spec.ts b/web/e2e/multispeed.spec.ts index 88b41d6..5262e56 100644 --- a/web/e2e/multispeed.spec.ts +++ b/web/e2e/multispeed.spec.ts @@ -51,7 +51,7 @@ test('complete operator workflow through the real backend and fake provider exec expect(providersResponse.ok()).toBe(true) const providers = await providersResponse.json() as Array<{ id: string; available: boolean; version: string }> expect(providers.find((provider) => provider.id === 'ookla')).toMatchObject({ available: false }) - expect(providers.find((provider) => provider.id === 'librespeed')).toMatchObject({ available: true, version: 'librespeed-cli v1.0.13+multispeed.dns2.xnet055 deterministic-e2e' }) + expect(providers.find((provider) => provider.id === 'librespeed')).toMatchObject({ available: true, version: 'librespeed-cli v1.0.13+multispeed.dns2.xnet056 deterministic-e2e' }) await page.goto('/settings') await expect(page.getByText('Ookla provider terms & authorization')).toBeVisible()