Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}

Expand All @@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <address>`, `--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 <address>`, `--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.

Expand Down
2 changes: 1 addition & 1 deletion internal/execution/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion internal/providers/librespeed/librespeed.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Adapter struct {
}

const (
requiredDNSPatchMarker = "+multispeed.dns2.xnet055"
requiredDNSPatchMarker = "+multispeed.dns2.xnet056"
allowedServerEndpointsEnvironment = "MULTISPEED_PROVIDER_ALLOWED_SERVER_ENDPOINTS"
)

Expand Down
4 changes: 2 additions & 2 deletions internal/providers/librespeed/librespeed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion scripts/docker-smoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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":""}]'''
Expand Down
2 changes: 1 addition & 1 deletion scripts/docker-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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":""}]'
Expand Down
6 changes: 3 additions & 3 deletions third_party/librespeed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion third_party/librespeed/build-corresponding-source.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
4 changes: 2 additions & 2 deletions web/e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion web/e2e/fixtures/librespeed-cli
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
Expand Down
2 changes: 1 addition & 1 deletion web/e2e/multispeed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading