diff --git a/.github/workflows/gateway-conformance.yml b/.github/workflows/gateway-conformance.yml index 20ec4c7ffda..e16a7977a37 100644 --- a/.github/workflows/gateway-conformance.yml +++ b/.github/workflows/gateway-conformance.yml @@ -60,11 +60,17 @@ jobs: run: make build working-directory: kubo-gateway - # 3. Init the kubo-gateway + # 3. Init the kubo-gateway. Every Addresses.* port is set explicitly + # and chosen unique across all three conformance jobs (leading 1xxxx + # for this one, 2xxxx and 3xxxx for the libp2p and cleartext jobs) + # so each job's footprint is unambiguous. - name: Init kubo-gateway run: | ./ipfs init -e ./ipfs config --json Gateway.PublicGateways "$GATEWAY_PUBLIC_GATEWAYS" + ./ipfs config --json Addresses.Swarm '["/ip4/127.0.0.1/tcp/14001"]' + ./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/18080" + ./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/15001" working-directory: kubo-gateway/cmd/ipfs # 4. Populate the Kubo gateway with the gateway-conformance fixtures @@ -95,8 +101,8 @@ jobs: - name: Run gateway-conformance tests uses: ipfs/gateway-conformance/.github/actions/test@v0.13 with: - gateway-url: http://127.0.0.1:8080 - subdomain-url: http://localhost:8080 + gateway-url: http://127.0.0.1:18080 + subdomain-url: http://localhost:18080 args: -skip 'TestGatewayCar/GET_response_for_application/vnd.ipld.car/Header_Content-Length' json: output.json xml: output.xml @@ -146,14 +152,19 @@ jobs: run: make build working-directory: kubo-gateway - # 3. Init the kubo-gateway + # 3. Init the kubo-gateway. The trustless subset this job runs does + # not exercise subdomain routing, so Gateway.PublicGateways is + # unnecessary here. Ports are in the 2xxxx range so each conformance + # job's footprint is unambiguous (1xxxx for the full job, 3xxxx for + # the cleartext job). - name: Init kubo-gateway run: | ./ipfs init --profile=test - ./ipfs config --json Gateway.PublicGateways "$GATEWAY_PUBLIC_GATEWAYS" - ./ipfs config --json Experimental.GatewayOverLibp2p true - ./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/8080" - ./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5001" + ./ipfs config --json HTTPProvider.Enabled true + ./ipfs config --json HTTPProvider.Libp2p true + ./ipfs config --json Addresses.Swarm '["/ip4/127.0.0.1/tcp/24001"]' + ./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/28080" + ./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/25001" working-directory: kubo-gateway/cmd/ipfs # 4. Populate the Kubo gateway with the gateway-conformance fixtures @@ -176,8 +187,9 @@ jobs: run: | ./ipfs init --profile=test -e ./ipfs config --json Experimental.Libp2pStreamMounting true - ./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/8081" - ./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5002" + ./ipfs config --json Addresses.Swarm '["/ip4/127.0.0.1/tcp/24002"]' + ./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/28081" + ./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/25002" working-directory: kubo-gateway/cmd/ipfs # 7. Start the kubo http-p2p-proxy @@ -192,16 +204,16 @@ jobs: # 8. Start forwarding data from the http-p2p-proxy to the node serving the Gateway API over libp2p - name: Start http-over-libp2p forwarding proxy run: | - gatewayNodeId=$(./ipfs --api=/ip4/127.0.0.1/tcp/5001 id -f="") - ./ipfs --api=/ip4/127.0.0.1/tcp/5002 swarm connect $(./ipfs --api=/ip4/127.0.0.1/tcp/5001 swarm addrs local --id | head -n 1) - ./ipfs --api=/ip4/127.0.0.1/tcp/5002 p2p forward --allow-custom-protocol /http/1.1 /ip4/127.0.0.1/tcp/8092 /p2p/$gatewayNodeId + gatewayNodeId=$(./ipfs --api=/ip4/127.0.0.1/tcp/25001 id -f="") + ./ipfs --api=/ip4/127.0.0.1/tcp/25002 swarm connect $(./ipfs --api=/ip4/127.0.0.1/tcp/25001 swarm addrs local --id | head -n 1) + ./ipfs --api=/ip4/127.0.0.1/tcp/25002 p2p forward --allow-custom-protocol /http/1.1 /ip4/127.0.0.1/tcp/28092 /p2p/$gatewayNodeId working-directory: kubo-gateway/cmd/ipfs # 9. Run the gateway-conformance tests over libp2p - name: Run gateway-conformance tests over libp2p uses: ipfs/gateway-conformance/.github/actions/test@v0.13 with: - gateway-url: http://127.0.0.1:8092 + gateway-url: http://127.0.0.1:28092 args: --specs "trustless-gateway,-trustless-ipns-gateway" -skip 'TestGatewayCar/GET_response_for_application/vnd.ipld.car/Header_Content-Length' json: output.json xml: output.xml @@ -224,3 +236,94 @@ jobs: with: name: gateway-conformance-libp2p.json path: output.json + + # Testing the trustless gateway subset exposed by HTTPProvider over plain + # HTTP/2 (h2c) on the swarm port. HTTPProvider.Cleartext auto-appends a /ws + # listener to each /tcp listener in Addresses.Swarm; HTTPProvider then + # shares the same TCP port via the shared-TCP demuxer and serves /http + # there. No libp2p proxy is needed: the conformance client connects to the + # swarm port directly. Complements the libp2p job above so both + # HTTPProvider transports are exercised end-to-end. + gateway-conformance-http-provider-cleartext: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + # 1. Download the gateway-conformance fixtures + - name: Download gateway-conformance fixtures + uses: ipfs/gateway-conformance/.github/actions/extract-fixtures@v0.13 + with: + output: fixtures + + # 2. Build the kubo-gateway + - name: Checkout kubo-gateway + uses: actions/checkout@v6 + with: + path: kubo-gateway + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: 'kubo-gateway/go.mod' + cache: true + cache-dependency-path: kubo-gateway/go.sum + - name: Build kubo-gateway + run: make build + working-directory: kubo-gateway + + # 3. Init the kubo-gateway with HTTPProvider.Cleartext on the swarm + # port. Libp2p is off; Cleartext is the only HTTPProvider transport + # exercised by this job. The trustless subset run below does not + # exercise subdomain routing, so Gateway.PublicGateways is + # unnecessary here. Ports are in the 3xxxx range so each + # conformance job's footprint is unambiguous (1xxxx for the full + # job, 2xxxx for the libp2p job). + - name: Init kubo-gateway + run: | + ./ipfs init --profile=test + ./ipfs config --json HTTPProvider.Enabled true + ./ipfs config --json HTTPProvider.Libp2p false + ./ipfs config --json HTTPProvider.Cleartext true + ./ipfs config --json Addresses.Swarm '["/ip4/127.0.0.1/tcp/34001"]' + ./ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/38080" + ./ipfs config Addresses.API "/ip4/127.0.0.1/tcp/35001" + working-directory: kubo-gateway/cmd/ipfs + + # 4. Populate the Kubo gateway with the gateway-conformance fixtures + - name: Import fixtures + run: | + # Import car files + find ./fixtures -name '*.car' -exec kubo-gateway/cmd/ipfs/ipfs dag import --pin-roots=false {} \; + + # 5. Start the kubo-gateway + - name: Start kubo-gateway + run: | + ( ./ipfs daemon & ) | sed '/Daemon is ready/q' + while [[ "$(./ipfs id | jq '.Addresses | length')" == '0' ]]; do sleep 1; done + working-directory: kubo-gateway/cmd/ipfs + + # 6. Run the gateway-conformance tests over plain HTTP/2 (h2c) on the swarm port + - name: Run gateway-conformance tests over h2c + uses: ipfs/gateway-conformance/.github/actions/test@v0.13 + with: + gateway-url: http://127.0.0.1:34001 + args: --specs "trustless-gateway,-trustless-ipns-gateway" -skip 'TestGatewayCar/GET_response_for_application/vnd.ipld.car/Header_Content-Length' + json: output.json + xml: output.xml + html: output.html + markdown: output.md + + # 7. Upload the results + - name: Upload MD summary + if: failure() || success() + run: cat output.md >> $GITHUB_STEP_SUMMARY + - name: Upload HTML report + if: failure() || success() + uses: actions/upload-artifact@v7 + with: + name: gateway-conformance-http-provider-cleartext.html + path: output.html + - name: Upload JSON report + if: failure() || success() + uses: actions/upload-artifact@v7 + with: + name: gateway-conformance-http-provider-cleartext.json + path: output.json diff --git a/.github/workflows/gotest.yml b/.github/workflows/gotest.yml index 89e0cc564f4..cc66e865b3a 100644 --- a/.github/workflows/gotest.yml +++ b/.github/workflows/gotest.yml @@ -202,6 +202,28 @@ jobs: fusermount3 -uz "$mp" 2>/dev/null || fusermount -uz "$mp" 2>/dev/null || true done + # AutoTLS end-to-end canary (in-process Pebble + p2p-forge). + # Isolated sub-module (test/autotls); heavy CoreDNS + Pebble deps stay + # out of the main go.mod. + autotls-tests: + if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch' + runs-on: ${{ fromJSON(github.repository == 'ipfs/kubo' && '["self-hosted", "linux", "x64", "2xlarge"]' || '"ubuntu-latest"') }} + timeout-minutes: 8 + env: + GOTRACEBACK: all + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + - name: Build kubo binary + run: make build + - name: Run AutoTLS end-to-end canary + run: make test_autotls + # Example tests (kubo-as-a-library) example-tests: if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch' diff --git a/.gitignore b/.gitignore index 09c29ed3d09..e77594bbe68 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ docs/examples/go-ipfs-as-a-library/example-folder/Qm* /test/cli/cli-tests.json /test/fuse/fuse-unit-tests.json /test/fuse/fuse-cli-tests.json +/test/autotls/autotls-tests.json # ignore build output from snapcraft /ipfs_*.snap diff --git a/AGENTS.md b/AGENTS.md index b6217483e15..9f6b78baf77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ make -O test_go_lint # run linter (use this instead of golangci-lint directly) If you modify `go.mod` (add/remove/update dependencies), you must run `make mod_tidy` first, before building or testing. Use `make mod_tidy` instead of `go mod tidy` directly, as the project has multiple `go.mod` files. -If you modify any `.go` files outside of `test/`, you must run `make build` before running integration tests. +If you modify any `.go` files outside of `test/`, you must run `make build` before running integration tests. Integration tests spawn a real `ipfs daemon` from `cmd/ipfs/ipfs` and `go test ./test/cli/...` does not rebuild that binary, so daemon-side changes (config schema, FX providers, transport options, default values) appear to be silently ignored when the binary is stale. Common symptoms: a new config flag has no effect, an expected listener never binds, an FX-injected handler is nil. If a CLI test behaves differently from a manual `cmd/ipfs/ipfs daemon` run, suspect a stale binary first. ## Testing diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a0cdc1948d..b96bf77f10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Kubo Changelogs +- [v0.44](docs/changelogs/v0.44.md) - [v0.43](docs/changelogs/v0.43.md) - [v0.42](docs/changelogs/v0.42.md) - [v0.41](docs/changelogs/v0.41.md) diff --git a/Rules.mk b/Rules.mk index 418aec15b15..db44137d9df 100644 --- a/Rules.mk +++ b/Rules.mk @@ -139,6 +139,7 @@ help: @echo ' test_unit - Run unit tests with coverage (excludes test/cli)' @echo ' test_cli - Run CLI integration tests (requires built binary)' @echo ' test_fuse - Run FUSE tests (requires /dev/fuse and fusermount)' + @echo ' test_autotls - Run AutoTLS end-to-end canary (in-process Pebble + p2p-forge)' @echo ' test_go_fmt - Check Go source formatting' @echo ' test_go_build - Build kubo for all platforms from .github/build-platforms.yml' @echo ' test_go_lint - Run golangci-lint' diff --git a/cmd/ipfs/kubo/daemon.go b/cmd/ipfs/kubo/daemon.go index 567da2fbe92..4246a595dfd 100644 --- a/cmd/ipfs/kubo/daemon.go +++ b/cmd/ipfs/kubo/daemon.go @@ -398,6 +398,17 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment return err } + // Reject the removed Experimental.GatewayOverLibp2p flag at startup so + // stale configs surface a clear migration error instead of silently + // running the wrong feature set. + if cfg.Experimental.GatewayOverLibp2p { + return errors.New( + "Experimental.GatewayOverLibp2p has been removed; " + + "set HTTPProvider.Enabled=true and HTTPProvider.Libp2p=true instead, " + + "then unset Experimental.GatewayOverLibp2p", + ) + } + // Validate autoconf setup - check for private network conflict swarmKey, _ := repo.SwarmKey() isPrivateNetwork := swarmKey != nil || pnet.ForcePrivateNetwork @@ -699,8 +710,8 @@ take effect. return err } - // add trustless gateway over libp2p - p2pGwErrc, err := serveTrustlessGatewayOverLibp2p(cctx) + // start the HTTPProvider libp2p-stream transport + httpProviderErrc, err := serveHTTPProviderOverLibp2p(cctx) if err != nil { return err } @@ -821,7 +832,7 @@ take effect. // collect long-running errors and block for shutdown // TODO(cryptix): our fuse currently doesn't follow this pattern for graceful shutdown var errs []error - for err := range merge(apiErrc, gwErrc, gcErrc, p2pGwErrc, pluginErrc, unmountErrc) { + for err := range merge(apiErrc, gwErrc, gcErrc, httpProviderErrc, pluginErrc, unmountErrc) { if err != nil { errs = append(errs, err) } @@ -1169,22 +1180,55 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e return errc, nil } -const gatewayProtocolID protocol.ID = "/ipfs/gateway" // FIXME: specify https://github.com/ipfs/specs/issues/433 +// gatewayProtocolID is the libp2p protocol ID under which the trustless +// gateway HTTP semantics are exposed over a libp2p stream, per the +// libp2p Gateway specification: +// https://specs.ipfs.tech/http-gateways/libp2p-gateway/ +const gatewayProtocolID protocol.ID = "/ipfs/gateway" + +// newGatewayWellKnown builds a fresh WellKnownHandler populated with the +// libp2p Gateway protocol meta. Used to publish /.well-known/libp2p/protocols +// on the HTTPProvider HTTPS path so clients can discover that the endpoint +// speaks the libp2p Gateway protocol. The libp2p-stream path uses its own +// WellKnownHandler attached to p2phttp.Host; both report the same metadata. +func newGatewayWellKnown() *p2phttp.WellKnownHandler { + wk := &p2phttp.WellKnownHandler{} + wk.AddProtocolMeta(gatewayProtocolID, p2phttp.ProtocolMeta{Path: "/"}) + return wk +} -func serveTrustlessGatewayOverLibp2p(cctx *oldcmds.Context) (<-chan error, error) { +// serveHTTPProviderOverLibp2p installs the HTTPProvider feature's transports +// once IpfsNode is fully constructed. It always installs the trustless +// gateway handler on the AutoWSS-port placeholder (the shared TCP path used +// by both AutoTLS and HTTPProvider.Cleartext) when HTTPProvider is enabled, +// and additionally starts the libp2p-stream transport when +// HTTPProvider.Libp2p is on, mounting the same handler under the +// /ipfs/gateway libp2p protocol ID per the libp2p Gateway specification +// (https://specs.ipfs.tech/http-gateways/libp2p-gateway/). +// +// The handler itself is the same across transports: NoFetch (only blocks +// already in the local blockstore), no DNSLink, no HTML errors, raw blocks +// via ?format=raw only. Deserialized UnixFS responses are not served, so +// clients always get content-addressed bytes they can verify against the +// requested CID. +func serveHTTPProviderOverLibp2p(cctx *oldcmds.Context) (<-chan error, error) { node, err := cctx.ConstructNode() if err != nil { - return nil, fmt.Errorf("serveHTTPGatewayOverLibp2p: ConstructNode() failed: %s", err) + return nil, fmt.Errorf("serveHTTPProviderOverLibp2p: ConstructNode() failed: %s", err) } cfg, err := node.Repo.Config() if err != nil { return nil, fmt.Errorf("could not read config: %w", err) } - if !cfg.Experimental.GatewayOverLibp2p { + noop := func() <-chan error { errCh := make(chan error) close(errCh) - return errCh, nil + return errCh + } + + if !cfg.HTTPProvider.Enabled.WithDefault(config.DefaultHTTPProviderEnabled) { + return noop(), nil } opts := []corehttp.ServeOption{ @@ -1198,14 +1242,44 @@ func serveTrustlessGatewayOverLibp2p(cctx *oldcmds.Context) (<-chan error, error return nil, err } + // Install the trustless handler on the AutoWSS-port placeholder. This + // path serves the gateway on the shared TCP port behind any /ws or + // /tls/ws listener: AutoTLS-issued /tls/http, manually configured /ws, + // and HTTPProvider.Cleartext-derived /ws. The placeholder is provided + // by FX whenever HTTPProvider.Enabled is true (see core/node/groups.go), + // independently of HTTPProvider.Libp2p below. + // + // The HTTPS mux also serves /.well-known/libp2p/protocols so HTTPS + // clients can discover that this peer speaks /ipfs/gateway (and any + // other libp2p protocols mounted in the future), per the libp2p+HTTP + // spec (https://specs.ipfs.tech/http-gateways/libp2p-gateway/). + // + // Wrap with RequireHTTP2OverTLS so the HTTPS path is h2-only (matches + // modern public HTTPS endpoints, gives bitswap-httpnet multiplexing) + // while the plain /ws path stays permissive for reverse-proxy interop. + if node.HTTPProvider != nil { + mux := http.NewServeMux() + mux.Handle("/", handler) + mux.Handle(p2phttp.WellKnownProtocols, newGatewayWellKnown()) + node.HTTPProvider.Set(libp2p.RequireHTTP2OverTLS(mux)) + log.Info("HTTPProvider: trustless gateway exposed on /ws and /tls/ws TCP ports (h2 required over TLS, h1+h2c allowed cleartext, .well-known/libp2p/protocols served)") + } + + // libp2p-stream transport. Gated by the Libp2p sub-toggle, independent + // of the AutoWSS-port install above. + if !cfg.HTTPProvider.Libp2p.WithDefault(config.DefaultHTTPProviderLibp2p) { + return noop(), nil + } + if node.PeerHost == nil { return nil, fmt.Errorf("cannot create libp2p gateway: node PeerHost is nil (this should not happen and likely indicates an FX dependency injection issue or race condition)") } + // libp2p-stream gateway. p2phttp.Host.Serve registers + // /.well-known/libp2p/protocols on h.ServeMux for us. h := p2phttp.Host{ StreamHost: node.PeerHost, } - h.WellKnownHandler.AddProtocolMeta(gatewayProtocolID, p2phttp.ProtocolMeta{Path: "/"}) h.ServeMux = http.NewServeMux() h.ServeMux.Handle("/", handler) diff --git a/config/autotls.go b/config/autotls.go index 4d90b717166..616c97532cb 100644 --- a/config/autotls.go +++ b/config/autotls.go @@ -40,15 +40,44 @@ type AutoTLS struct { // Optional, controls if features like AutoWSS should generate shorter /dnsX instead of /ipX/../sni/.. ShortAddrs Flag `json:",omitempty"` + + // SelfSignedForTests is a test-only escape hatch. When true, the daemon + // skips the AutoTLS / p2p-forge / ACME pipeline entirely and provides + // the WebSocket transport with an in-memory self-signed TLS config. + // Test clients pair this with tls.Config{InsecureSkipVerify: true} to + // drive the /tls/ws and /tls/http paths without real ACME issuance. + // + // Never set this in a production config. The cert is regenerated on + // every daemon start and is not trusted by any browser or CA. + SelfSignedForTests Flag `json:",omitempty"` + + // TrustedCARootsPEM is an optional PEM-encoded bundle of CA + // certificates that the ACME client trusts in addition to the system + // trust store. Set this when AutoTLS.CAEndpoint points at a CA whose + // root is not in the system store: private or self-hosted ACME + // deployments, and the in-process Pebble used by the AutoTLS E2E test + // in test/autotls/. + TrustedCARootsPEM *OptionalString `json:",omitempty"` + + // AllowPrivateForgeAddrs lifts the p2p-forge client's requirement that + // the libp2p host report a publicly reachable address before requesting + // a certificate. Set this for private/intranet libp2p deployments + // (where reachability is asymmetric or implicit) and for the AutoTLS + // E2E test in test/autotls/, which runs entirely on loopback. + // + // Leave this off in normal public deployments; the default behavior + // avoids wasting ACME issuance on a node that no one can reach. + AllowPrivateForgeAddrs Flag `json:",omitempty"` } const ( - DefaultAutoTLSEnabled = true // with DefaultAutoTLSRegistrationDelay, unless explicitly enabled in config - DefaultDomainSuffix = p2pforge.DefaultForgeDomain - DefaultRegistrationEndpoint = p2pforge.DefaultForgeEndpoint - DefaultCAEndpoint = p2pforge.DefaultCAEndpoint - DefaultAutoWSS = true // requires AutoTLS.Enabled - DefaultAutoTLSShortAddrs = true // requires AutoTLS.Enabled - DefaultAutoTLSSkipDNSLookup = true // skip network DNS for p2p-forge domains - DefaultAutoTLSRegistrationDelay = 1 * time.Hour + DefaultAutoTLSEnabled = true // with DefaultAutoTLSRegistrationDelay, unless explicitly enabled in config + DefaultDomainSuffix = p2pforge.DefaultForgeDomain + DefaultRegistrationEndpoint = p2pforge.DefaultForgeEndpoint + DefaultCAEndpoint = p2pforge.DefaultCAEndpoint + DefaultAutoWSS = true // requires AutoTLS.Enabled + DefaultAutoTLSShortAddrs = true // requires AutoTLS.Enabled + DefaultAutoTLSSkipDNSLookup = true // skip network DNS for p2p-forge domains + DefaultAutoTLSRegistrationDelay = 1 * time.Hour + DefaultAutoTLSSelfSignedForTests = false ) diff --git a/config/config.go b/config/config.go index 0952e0d3173..c24c52b9086 100644 --- a/config/config.go +++ b/config/config.go @@ -39,6 +39,7 @@ type Config struct { Provider Provider // Deprecated: use Provide. Will be removed in a future release. Reprovider Reprovider // Deprecated: use Provide. Will be removed in a future release. HTTPRetrieval HTTPRetrieval + HTTPProvider HTTPProvider Experimental Experiments Plugins Plugins Pinning Pinning diff --git a/config/experiments.go b/config/experiments.go index 6c43ac04f07..a635420781d 100644 --- a/config/experiments.go +++ b/config/experiments.go @@ -9,7 +9,7 @@ type Experiments struct { StrategicProviding bool `json:",omitempty"` // removed, use Provider.Enabled instead OptimisticProvide bool OptimisticProvideJobsPoolSize int - GatewayOverLibp2p bool `json:",omitempty"` + GatewayOverLibp2p bool `json:",omitempty"` // Deprecated: use HTTPProvider.Enabled instead. GraphsyncEnabled graphsyncEnabled `json:",omitempty"` AcceleratedDHTClient experimentalAcceleratedDHTClient `json:",omitempty"` diff --git a/config/httpprovider.go b/config/httpprovider.go new file mode 100644 index 00000000000..45150d18244 --- /dev/null +++ b/config/httpprovider.go @@ -0,0 +1,68 @@ +package config + +// HTTPProvider configures kubo's role as an HTTP-native source of +// trustless-gateway block retrieval. When Enabled, the local trustless +// gateway handler (NoFetch, raw blocks via ?format=raw only, +// content-addressed verification client-side) is exposed for HTTP +// retrieval clients such as boxo/bitswap/network/httpnet. +// +// This is the server side of the HTTP retrieval story; the client side +// lives separately under HTTPRetrieval. +// +// Defaults live on the DefaultHTTPProvider* constants below. +type HTTPProvider struct { + // Enabled is the master switch. When true, the trustless gateway + // handler is registered for the libp2p-stream transport (see + // Libp2p). Cleartext and AnnounceMultiaddrs gate further surfaces + // and must each be set explicitly. + Enabled Flag `json:",omitempty"` + + // Libp2p exposes the trustless gateway over a libp2p stream, per the + // libp2p Gateway spec (https://specs.ipfs.tech/http-gateways/libp2p-gateway/). + // The handler mounts under the /ipfs/gateway protocol ID and is + // advertised via .well-known/libp2p/protocols on the libp2p+HTTP + // host. + Libp2p Flag `json:",omitempty"` + + // Cleartext auto-appends a plaintext /ws listener to each /tcp/N in + // Addresses.Swarm, unless one is already present. The new /ws shares + // the existing TCP port via the shared-TCP demuxer, so no extra + // socket is opened. + // + // Intended for deployments where a reverse proxy (Caddy, Traefik, + // nginx, etc.) terminates TLS in front of kubo and forwards HTTP/1.1 + // or h2c to this node. With AutoTLS, kubo already serves /tls/ws and + // /tls/http with a Let's Encrypt cert, so a cleartext path would + // expose the trustless gateway and WebSocket upgrade unencrypted on + // the public network. Turn it on knowingly. + // + // The matching /http multiaddr announcement is controlled by + // AnnounceMultiaddrs. + Cleartext Flag `json:",omitempty"` + + // AnnounceMultiaddrs derives an HTTP-flavored multiaddr from each + // WebSocket listener and adds it to the announced address set: + // /ws to /http, /tls/ws to /tls/http, /tls/sni//ws to + // /tls/sni//http. The HTTP endpoint shares the WebSocket + // listener's TCP port and TLS cert; no extra socket is opened. HTTP + // retrieval clients (e.g. boxo/bitswap/network/httpnet) then discover + // this peer as an HTTP source through identify, the DHT, and IPNI + // without out-of-band knowledge. + // + // Subject to Addresses.NoAnnounce filters, like any other announced + // multiaddr. + AnnounceMultiaddrs Flag `json:",omitempty"` +} + +// HTTPProvider defaults. The master switch is off, so the feature is +// inert until an operator opts in. Once Enabled, the libp2p-stream +// transport comes up automatically because the .well-known descriptor +// already advertises that path; the cleartext listener and the /http +// announcement stay off so operators broadcast HTTP availability on +// purpose, not by accident. +const ( + DefaultHTTPProviderEnabled = false + DefaultHTTPProviderLibp2p = true + DefaultHTTPProviderCleartext = false + DefaultHTTPProviderAnnounceMultiaddrs = false +) diff --git a/core/core.go b/core/core.go index 5f37c287116..9dda377bef7 100644 --- a/core/core.go +++ b/core/core.go @@ -122,6 +122,12 @@ type IpfsNode struct { P2P *p2p.P2P `optional:"true"` + // HTTPProvider receives the trustless gateway handler that the WebSocket + // transport routes non-WebSocket requests to. Set when HTTPProvider and + // AutoTLS are both enabled. See + // `cmd/ipfs/kubo/daemon.go:serveHTTPProviderOverLibp2p`. + HTTPProvider *libp2p.HTTPProviderHandler `optional:"true"` + ctx context.Context stop func() error diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index 43452cdd9b6..89c40ee86f3 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -99,8 +99,8 @@ func Libp2pGatewayOption() ServeOption { bserv := blockservice.New(n.Blocks.Blockstore(), offline.Exchange(n.Blocks.Blockstore())) backend, err := gateway.NewBlocksBackend(bserv, - // GatewayOverLibp2p only returns things that are in local blockstore - // (same as Gateway.NoFetch=true), we have to pass offline path resolver + // HTTPProvider only returns blocks already in the local blockstore + // (same as Gateway.NoFetch=true), so we hand it the offline path resolver. gateway.WithResolver(n.OfflineUnixFSPathResolver), ) if err != nil { diff --git a/core/node/groups.go b/core/node/groups.go index ad65c48bd04..8a7faca84e4 100644 --- a/core/node/groups.go +++ b/core/node/groups.go @@ -120,6 +120,8 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part enableRelayClient := cfg.Swarm.RelayClient.Enabled.WithDefault(enableRelayTransport) enableAutoTLS := cfg.AutoTLS.Enabled.WithDefault(config.DefaultAutoTLSEnabled) enableAutoWSS := cfg.AutoTLS.AutoWSS.WithDefault(config.DefaultAutoWSS) + // HTTPProvider master switch. + enableHTTPProvider := cfg.HTTPProvider.Enabled.WithDefault(config.DefaultHTTPProviderEnabled) atlsLog := log.Logger("autotls") // Log error when relay subsystem could not be initialized due to missing dependency @@ -189,6 +191,39 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part enableAutoTLS = false } + // HTTPProvider.Cleartext: auto-append a plaintext /ws listener to each + // /tcp/N already in Addresses.Swarm so the HTTPProvider handler is + // reachable over cleartext (typical reverse-proxy deployment). The new + // /ws shares the existing TCP socket via tcpreuse; AutoWSS-installed + // /tls/sni//ws covers the TLS path and is left untouched. + // + // Skipped only when the user has a *cleartext* /ws listener already + // configured. /wss and /tls/ws are TLS-WebSocket forms, not cleartext, + // so they do not pre-empt the cleartext auto-append. + enableHTTPProviderCleartext := cfg.HTTPProvider.Cleartext.WithDefault(config.DefaultHTTPProviderCleartext) + if enableHTTPProvider && enableHTTPProviderCleartext && enableTCPTransport && enableWebsocketTransport { + plainWsRegex := regexp.MustCompile(`/tcp/\d+/ws$`) // cleartext only; excludes /wss and /tls/ws + tcpRegex := regexp.MustCompile(`/tcp/\d+$`) + plainWsPresent := false + var tcpListeners []string + for _, listener := range cfg.Addresses.Swarm { + if plainWsRegex.MatchString(listener) { + plainWsPresent = true + break + } + if tcpRegex.MatchString(listener) { + tcpListeners = append(tcpListeners, listener) + } + } + if !plainWsPresent { + for _, tcpListener := range tcpListeners { + wsListener := tcpListener + "/ws" + cfg.Addresses.Swarm = append(cfg.Addresses.Swarm, wsListener) + atlsLog.Infof("HTTPProvider.Cleartext: appended cleartext /ws listener: %s", wsListener) + } + } + } + // Gather all the options opts := fx.Options( BaseLibP2P, @@ -200,14 +235,31 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part fx.Provide(libp2p.ResourceManager(bcfg.Repo.Path(), cfg.Swarm, userResourceOverrides)), maybeProvide(libp2p.P2PForgeCertMgr(bcfg.Repo.Path(), cfg.AutoTLS, atlsLog), enableAutoTLS), maybeInvoke(libp2p.StartP2PAutoTLS, enableAutoTLS), + // AutoTLS.SelfSignedForTests: bypass the entire AutoTLS / p2p-forge + // pipeline and hand the WebSocket transport an in-memory self-signed + // cert. Test escape hatch only. + maybeProvide(libp2p.NewSelfSignedTestTLSConfig, cfg.AutoTLS.SelfSignedForTests.WithDefault(config.DefaultAutoTLSSelfSignedForTests)), + // When HTTPProvider is enabled, register a placeholder fallback + // handler so the WebSocket transport can route non-upgrade requests + // on every /ws or /tls/ws listener to the trustless gateway. The + // real handler is installed by daemon.go once IpfsNode is up. The + // gate is intentionally independent of AutoTLS because cleartext + // /ws (manual or HTTPProvider.Cleartext-derived) needs the handler + // too. + maybeProvide(libp2p.NewHTTPProviderHandler, enableHTTPProvider), fx.Provide(libp2p.AddrFilters(cfg.Swarm.AddrFilters)), maybeInvoke(libp2p.MonitorDeadListeners(cfg.Addresses.Swarm, cfg.Swarm.AddrFilters, cfg.Addresses.NoAnnounce), cfg.Internal.DeadListenerCheck.WithDefault(config.DefaultDeadListenerCheck)), maybeInvoke(libp2p.MonitorCGNAT(), cfg.Internal.CGNATCheck.WithDefault(config.DefaultCGNATCheck)), - fx.Provide(libp2p.AddrsFactory(cfg.Addresses.Announce, cfg.Addresses.AppendAnnounce, cfg.Addresses.NoAnnounce)), + fx.Provide(libp2p.AddrsFactory( + cfg.Addresses.Announce, + cfg.Addresses.AppendAnnounce, + cfg.Addresses.NoAnnounce, + enableHTTPProvider && cfg.HTTPProvider.AnnounceMultiaddrs.WithDefault(config.DefaultHTTPProviderAnnounceMultiaddrs), + )), fx.Provide(libp2p.SmuxTransport(cfg.Swarm.Transports)), fx.Provide(libp2p.RelayTransport(enableRelayTransport)), fx.Provide(libp2p.RelayService(enableRelayService, cfg.Swarm.RelayService)), - fx.Provide(libp2p.Transports(cfg.Swarm.Transports)), + fx.Provide(libp2p.Transports(cfg.Swarm.Transports, cfg.Gateway.RetrievalTimeout.WithDefault(config.DefaultRetrievalTimeout))), fx.Provide(libp2p.ListenOn(cfg.Addresses.Swarm)), fx.Invoke(libp2p.SetupDiscovery(cfg.Discovery.MDNS.Enabled)), fx.Provide(libp2p.ForceReachability(cfg.Internal.Libp2pForceReachability)), diff --git a/core/node/libp2p/addrs.go b/core/node/libp2p/addrs.go index 9670f787fcc..49c2e1a93b5 100644 --- a/core/node/libp2p/addrs.go +++ b/core/node/libp2p/addrs.go @@ -2,7 +2,11 @@ package libp2p import ( "context" + "crypto/x509" "fmt" + "net" + "net/http" + "net/url" "os" "path/filepath" "time" @@ -336,31 +340,84 @@ func makeAddrsFactory(announce []string, appendAnnounce []string, noAnnounce []s }, nil } -func AddrsFactory(announce []string, appendAnnounce []string, noAnnounce []string) any { +func AddrsFactory(announce []string, appendAnnounce []string, noAnnounce []string, announceHTTPProvider bool) any { return func(params struct { fx.In ForgeMgr *p2pforge.P2PForgeCertMgr `optional:"true"` }, ) (opts Libp2pOpts, err error) { - var addrsFactory p2pbhost.AddrsFactory announceAddrsFactory, err := makeAddrsFactory(announce, appendAnnounce, noAnnounce) if err != nil { return opts, err } - if params.ForgeMgr == nil { - addrsFactory = announceAddrsFactory - } else { - addrsFactory = func(multiaddrs []ma.Multiaddr) []ma.Multiaddr { - forgeProcessing := params.ForgeMgr.AddressFactory()(multiaddrs) - announceProcessing := announceAddrsFactory(forgeProcessing) - return announceProcessing + // The factory pipeline runs in this order so each step sees the + // output of the previous one: + // 1. ForgeMgr substitutes the wildcard SNI in /tls/sni/*./ws + // with the real per-peer hostname (or its short form). + // 2. HTTPProvider derives an HTTP-flavored multiaddr from each /ws + // so HTTP retrieval clients can discover this peer; runs after + // ForgeMgr so the SNI value in /tls/sni//http is already + // the resolved one, not the wildcard. + // 3. announceAddrsFactory applies Addresses.Announce/AppendAnnounce + // and drops anything matching Addresses.NoAnnounce, so the + // derived addresses are filtered just like every other one. + addrsFactory := func(multiaddrs []ma.Multiaddr) []ma.Multiaddr { + if params.ForgeMgr != nil { + multiaddrs = params.ForgeMgr.AddressFactory()(multiaddrs) + } + if announceHTTPProvider { + multiaddrs = appendHTTPProviderAddrs(multiaddrs) } + return announceAddrsFactory(multiaddrs) } opts.Opts = append(opts.Opts, libp2p.AddrsFactory(addrsFactory)) return } } +// httpComponent is the /http multiaddr protocol component that pairs with +// /ws on the same TCP port. We never listen on it; it is only ever appended +// to announced multiaddrs to advertise the HTTPProvider endpoint. +var httpComponent, _ = ma.NewComponent("http", "") + +// appendHTTPProviderAddrs returns a slice that contains every input +// multiaddr plus, for each one ending in /ws, an additional copy with /ws +// replaced by /http. Order is preserved; the /http variant immediately +// follows its /ws sibling. Multiaddrs that do not end in /ws pass through +// unchanged. Duplicates (which would arise if both /ws and /http were +// already in the input) are dropped. +func appendHTTPProviderAddrs(addrs []ma.Multiaddr) []ma.Multiaddr { + out := make([]ma.Multiaddr, 0, len(addrs)*2) + seen := make(map[string]struct{}, len(addrs)*2) + for _, a := range addrs { + if _, ok := seen[string(a.Bytes())]; !ok { + out = append(out, a) + seen[string(a.Bytes())] = struct{}{} + } + http, ok := wsToHTTP(a) + if !ok { + continue + } + if _, ok := seen[string(http.Bytes())]; !ok { + out = append(out, http) + seen[string(http.Bytes())] = struct{}{} + } + } + return out +} + +// wsToHTTP returns m with its trailing /ws component replaced by /http, +// and a boolean indicating whether the input ended in /ws. Everything +// before the trailing /ws (including any /tls and /tls/sni/ +// components) is preserved unchanged. +func wsToHTTP(m ma.Multiaddr) (ma.Multiaddr, bool) { + prefix, last := ma.SplitLast(m) + if last == nil || last.Protocol().Code != ma.P_WS { + return nil, false + } + return prefix.AppendComponent(httpComponent), true +} + func ListenOn(addresses []string) any { return func() (opts Libp2pOpts) { return Libp2pOpts{ @@ -390,18 +447,48 @@ func P2PForgeCertMgr(repoPath string, cfg config.AutoTLS, atlsLog *logging.ZapEv registrationDelay = 0 * time.Second } + // Pull the optional ?dial= and ?dns= test/debug overrides off the + // URL before handing it to p2p-forge. See docs/config.md. + regEndpoint, overrides, err := parseForgeOverrides(cfg.RegistrationEndpoint.WithDefault(config.DefaultRegistrationEndpoint)) + if err != nil { + return nil, fmt.Errorf("AutoTLS.RegistrationEndpoint: %w", err) + } + certStorage := &certmagic.FileStorage{Path: storagePath} - certMgr, err := p2pforge.NewP2PForgeCertMgr( + opts := []p2pforge.P2PForgeCertMgrOptions{ p2pforge.WithLogger(rawLogger.Sugar()), p2pforge.WithForgeDomain(cfg.DomainSuffix.WithDefault(config.DefaultDomainSuffix)), - p2pforge.WithForgeRegistrationEndpoint(cfg.RegistrationEndpoint.WithDefault(config.DefaultRegistrationEndpoint)), + p2pforge.WithForgeRegistrationEndpoint(regEndpoint), p2pforge.WithRegistrationDelay(registrationDelay), p2pforge.WithCAEndpoint(cfg.CAEndpoint.WithDefault(config.DefaultCAEndpoint)), p2pforge.WithForgeAuth(cfg.RegistrationToken.WithDefault(os.Getenv(p2pforge.ForgeAuthEnv))), p2pforge.WithUserAgent(version.GetUserAgentVersion()), p2pforge.WithCertificateStorage(certStorage), p2pforge.WithShortForgeAddrs(cfg.ShortAddrs.WithDefault(config.DefaultAutoTLSShortAddrs)), - ) + } + // AutoTLS.TrustedCARootsPEM: optional CA bundle for private or + // self-hosted ACME deployments (including the in-process Pebble + // used by the AutoTLS E2E test). + if pem := cfg.TrustedCARootsPEM.WithDefault(""); pem != "" { + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM([]byte(pem)) { + return nil, fmt.Errorf("AutoTLS.TrustedCARootsPEM did not contain any parseable certificate") + } + opts = append(opts, p2pforge.WithTrustedRoots(pool)) + } + // AutoTLS.AllowPrivateForgeAddrs: lift the "must be publicly + // reachable" gate on cert requests, for private/intranet + // deployments and the AutoTLS E2E test (loopback only). + if cfg.AllowPrivateForgeAddrs.WithDefault(false) { + opts = append(opts, p2pforge.WithAllowPrivateForgeAddrs()) + } + if overrides.dial != "" { + opts = append(opts, p2pforge.WithHTTPClient(forgeDialOverrideClient(overrides.dial))) + } + if overrides.dns != "" { + opts = append(opts, p2pforge.WithResolver(forgeDNSOverrideResolver(overrides.dns))) + } + certMgr, err := p2pforge.NewP2PForgeCertMgr(opts...) if err != nil { return nil, err } @@ -410,6 +497,74 @@ func P2PForgeCertMgr(repoPath string, cfg config.AutoTLS, atlsLog *logging.ZapEv } } +// forgeOverrides holds the test/debug overrides parsed off +// AutoTLS.RegistrationEndpoint. All fields are empty when the URL carries no +// overrides. See docs/config.md for the user-facing description. +type forgeOverrides struct { + dial string // ?dial=host:port: forces the registration POST to dial here + dns string // ?dns=host:port: forces DNS-01 pre-flight lookups to this server +} + +// parseForgeOverrides strips the recognized ?dial= and ?dns= query parameters +// from raw, returning the cleaned URL and the parsed overrides. Unrelated +// query parameters are preserved. +func parseForgeOverrides(raw string) (cleanURL string, ov forgeOverrides, err error) { + u, err := url.Parse(raw) + if err != nil { + return "", forgeOverrides{}, err + } + q := u.Query() + for _, p := range []struct { + key string + dst *string + }{ + {"dial", &ov.dial}, + {"dns", &ov.dns}, + } { + v := q.Get(p.key) + if v == "" { + continue + } + if _, _, err := net.SplitHostPort(v); err != nil { + return "", forgeOverrides{}, fmt.Errorf("%s=%q must be host:port: %w", p.key, v, err) + } + *p.dst = v + q.Del(p.key) + } + // Return raw unchanged when no override was set, so a no-op call + // doesn't reorder unrelated query parameters via Query().Encode(). + if ov == (forgeOverrides{}) { + return raw, ov, nil + } + u.RawQuery = q.Encode() + return u.String(), ov, nil +} + +// forgeDialOverrideClient returns an *http.Client whose Transport always dials +// dialAddr, ignoring the request URL's host. Used by the ?dial= override. +func forgeDialOverrideClient(dialAddr string) *http.Client { + dialer := &net.Dialer{} + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, "tcp", dialAddr) + }, + }, + } +} + +// forgeDNSOverrideResolver returns a *net.Resolver that sends every lookup to +// dnsAddr instead of the system resolver. Used by the ?dns= override. +func forgeDNSOverrideResolver(dnsAddr string) *net.Resolver { + dialer := &net.Dialer{} + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, network, dnsAddr) + }, + } +} + func StartP2PAutoTLS(lc fx.Lifecycle, certMgr *p2pforge.P2PForgeCertMgr, h host.Host) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { diff --git a/core/node/libp2p/addrs_test.go b/core/node/libp2p/addrs_test.go index f18da91b2ca..fc26650e3a7 100644 --- a/core/node/libp2p/addrs_test.go +++ b/core/node/libp2p/addrs_test.go @@ -19,6 +19,102 @@ func mustMultiaddrs(t *testing.T, addrs ...string) []ma.Multiaddr { return out } +// mustMultiaddrStrings is the inverse of mustMultiaddrs: a slice of strings +// for stable comparison in test expectations. +func mustMultiaddrStrings(addrs []ma.Multiaddr) []string { + out := make([]string, len(addrs)) + for i, a := range addrs { + out[i] = a.String() + } + return out +} + +func TestAppendHTTPProviderAddrs(t *testing.T) { + cases := []struct { + name string + in []string + want []string + }{ + { + name: "cleartext_ws", + in: []string{"/ip4/1.2.3.4/tcp/4001/ws"}, + want: []string{"/ip4/1.2.3.4/tcp/4001/ws", "/ip4/1.2.3.4/tcp/4001/http"}, + }, + { + name: "tls_ws", + in: []string{"/ip4/1.2.3.4/tcp/4001/tls/ws"}, + want: []string{"/ip4/1.2.3.4/tcp/4001/tls/ws", "/ip4/1.2.3.4/tcp/4001/tls/http"}, + }, + { + name: "tls_sni_ws", + in: []string{"/ip4/1.2.3.4/tcp/4001/tls/sni/example.libp2p.direct/ws"}, + want: []string{ + "/ip4/1.2.3.4/tcp/4001/tls/sni/example.libp2p.direct/ws", + "/ip4/1.2.3.4/tcp/4001/tls/sni/example.libp2p.direct/http", + }, + }, + { + name: "dns_tls_ws", + in: []string{"/dns4/example.com/tcp/443/tls/ws"}, + want: []string{"/dns4/example.com/tcp/443/tls/ws", "/dns4/example.com/tcp/443/tls/http"}, + }, + { + name: "non_ws_addr_unchanged", + in: []string{ + "/ip4/1.2.3.4/tcp/4001", + "/ip4/1.2.3.4/udp/4001/quic-v1", + }, + want: []string{ + "/ip4/1.2.3.4/tcp/4001", + "/ip4/1.2.3.4/udp/4001/quic-v1", + }, + }, + { + name: "mixed_preserves_order", + in: []string{ + "/ip4/1.2.3.4/tcp/4001", + "/ip4/1.2.3.4/tcp/4001/tls/ws", + "/ip4/1.2.3.4/udp/4001/quic-v1", + }, + want: []string{ + "/ip4/1.2.3.4/tcp/4001", + "/ip4/1.2.3.4/tcp/4001/tls/ws", + "/ip4/1.2.3.4/tcp/4001/tls/http", + "/ip4/1.2.3.4/udp/4001/quic-v1", + }, + }, + { + name: "preexisting_http_not_duplicated", + in: []string{ + "/ip4/1.2.3.4/tcp/4001/tls/ws", + "/ip4/1.2.3.4/tcp/4001/tls/http", + }, + want: []string{ + "/ip4/1.2.3.4/tcp/4001/tls/ws", + "/ip4/1.2.3.4/tcp/4001/tls/http", + }, + }, + { + name: "ws_appears_only_once_when_repeated", + in: []string{ + "/ip4/1.2.3.4/tcp/4001/tls/ws", + "/ip4/1.2.3.4/tcp/4001/tls/ws", + }, + want: []string{ + "/ip4/1.2.3.4/tcp/4001/tls/ws", + "/ip4/1.2.3.4/tcp/4001/tls/http", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := appendHTTPProviderAddrs(mustMultiaddrs(t, c.in...)) + require.Equal(t, c.want, mustMultiaddrStrings(got)) + }) + } +} + func TestFindDeadListeners(t *testing.T) { cases := []struct { name string @@ -293,3 +389,91 @@ func TestMakeAddrsFactoryDropsEmptyMultiaddrs(t *testing.T) { } } } + +// TestParseForgeOverrides exercises the ?dial=/?dns= query-string syntax used +// to redirect the forge registration request and DNS-01 propagation lookup +// during tests and local debugging. Both overrides are stripped from the URL +// handed back to p2p-forge; malformed values are rejected up-front. +func TestParseForgeOverrides(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + input string + wantURL string + wantDial string + wantDNS string + wantErrSub string + }{ + { + name: "no query", + input: "https://registration.libp2p.direct", + wantURL: "https://registration.libp2p.direct", + }, + { + name: "unrelated query preserved", + input: "https://registration.libp2p.direct?foo=bar", + wantURL: "https://registration.libp2p.direct?foo=bar", + }, + { + name: "dial only is stripped", + input: "http://registration.libp2p.test/?dial=127.0.0.1:42013", + wantURL: "http://registration.libp2p.test/", + wantDial: "127.0.0.1:42013", + }, + { + name: "dns only is stripped", + input: "http://registration.libp2p.test/?dns=127.0.0.1:5353", + wantURL: "http://registration.libp2p.test/", + wantDNS: "127.0.0.1:5353", + }, + { + name: "dial and dns together, both stripped", + input: "http://registration.libp2p.test/?dial=127.0.0.1:42013&dns=127.0.0.1:5353", + wantURL: "http://registration.libp2p.test/", + wantDial: "127.0.0.1:42013", + wantDNS: "127.0.0.1:5353", + }, + { + name: "overrides alongside unrelated params", + input: "http://example.test/?foo=bar&dial=127.0.0.1:9000&baz=qux&dns=127.0.0.1:5353", + wantURL: "http://example.test/?baz=qux&foo=bar", + wantDial: "127.0.0.1:9000", + wantDNS: "127.0.0.1:5353", + }, + { + name: "ipv6 addresses", + input: "http://example.test/?dial=[::1]:9000&dns=[::1]:5353", + wantURL: "http://example.test/", + wantDial: "[::1]:9000", + wantDNS: "[::1]:5353", + }, + { + name: "malformed dial rejected", + input: "http://example.test/?dial=not-a-host-port", + wantErrSub: `dial="not-a-host-port"`, + }, + { + name: "malformed dns rejected", + input: "http://example.test/?dns=not-a-host-port", + wantErrSub: `dns="not-a-host-port"`, + }, + { + name: "empty override values treated as absent", + input: "http://example.test/?dial=&dns=", + wantURL: "http://example.test/?dial=&dns=", + }, + } { + t.Run(tc.name, func(t *testing.T) { + gotURL, ov, err := parseForgeOverrides(tc.input) + if tc.wantErrSub != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrSub) + return + } + require.NoError(t, err) + require.Equal(t, tc.wantURL, gotURL) + require.Equal(t, tc.wantDial, ov.dial) + require.Equal(t, tc.wantDNS, ov.dns) + }) + } +} diff --git a/core/node/libp2p/autotls_selfsigned.go b/core/node/libp2p/autotls_selfsigned.go new file mode 100644 index 00000000000..9a275eafe6b --- /dev/null +++ b/core/node/libp2p/autotls_selfsigned.go @@ -0,0 +1,73 @@ +package libp2p + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "net" + "time" +) + +// SelfSignedTestTLSConfig is a *tls.Config carrying a freshly generated +// self-signed cert. Wrapped in a named type so FX can disambiguate it from +// any other *tls.Config value in the dependency graph. +type SelfSignedTestTLSConfig struct { + *tls.Config +} + +// NewSelfSignedTestTLSConfig generates an in-memory self-signed certificate +// and returns it wrapped as the FX-friendly SelfSignedTestTLSConfig. +// +// The cert covers `localhost`, `127.0.0.1`, and `::1`, plus the AutoTLS +// wildcard `*.libp2p.direct` so /tls/sni//ws listeners that the test +// configures by hand still match. The key is freshly generated each daemon +// start. Test clients must use tls.Config{InsecureSkipVerify: true} since +// no public CA signs this certificate. +// +// This provider is wired only when AutoTLS.SelfSignedForTests is true. +// It is a test escape hatch and must not be used in production. +func NewSelfSignedTestTLSConfig() (*SelfSignedTestTLSConfig, error) { + cert, err := generateSelfSignedTestCert() + if err != nil { + return nil, fmt.Errorf("self-signed test cert: %w", err) + } + return &SelfSignedTestTLSConfig{ + Config: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + }, nil +} + +func generateSelfSignedTestCert() (tls.Certificate, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return tls.Certificate{}, err + } + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{Organization: []string{"kubo test"}}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + DNSNames: []string{"localhost", "*.libp2p.direct"}, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, err + } + return tls.Certificate{ + Certificate: [][]byte{der}, + PrivateKey: priv, + }, nil +} diff --git a/core/node/libp2p/autotls_selfsigned_test.go b/core/node/libp2p/autotls_selfsigned_test.go new file mode 100644 index 00000000000..bdf832e1f52 --- /dev/null +++ b/core/node/libp2p/autotls_selfsigned_test.go @@ -0,0 +1,47 @@ +package libp2p + +import ( + "crypto/x509" + "net" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestSelfSignedTestTLSConfig spot-checks that the test-only cert covers +// the SANs the test harness depends on (loopback IPs and the AutoTLS +// wildcard) and is valid for at least the minute it takes a test to run. +func TestSelfSignedTestTLSConfig(t *testing.T) { + cfg, err := NewSelfSignedTestTLSConfig() + require.NoError(t, err) + require.NotNil(t, cfg) + require.Len(t, cfg.Certificates, 1) + + leaf, err := x509.ParseCertificate(cfg.Certificates[0].Certificate[0]) + require.NoError(t, err) + + require.True(t, leaf.NotAfter.After(time.Now().Add(time.Minute)), + "cert should be valid past the test window") + require.True(t, leaf.NotBefore.Before(time.Now()), + "cert should already be valid") + + require.True(t, ipAddressesContain(leaf.IPAddresses, "127.0.0.1"), + "cert must cover loopback IPv4") + require.True(t, ipAddressesContain(leaf.IPAddresses, "::1"), + "cert must cover loopback IPv6") + require.True(t, slices.Contains(leaf.DNSNames, "localhost")) + require.True(t, slices.Contains(leaf.DNSNames, "*.libp2p.direct"), + "cert must cover the AutoTLS wildcard so /tls/sni//ws listeners work in tests") +} + +func ipAddressesContain(haystack []net.IP, needle string) bool { + target := net.ParseIP(needle) + for _, ip := range haystack { + if ip.Equal(target) { + return true + } + } + return false +} diff --git a/core/node/libp2p/httpprovider.go b/core/node/libp2p/httpprovider.go new file mode 100644 index 00000000000..55f2ab78072 --- /dev/null +++ b/core/node/libp2p/httpprovider.go @@ -0,0 +1,74 @@ +package libp2p + +import ( + "net/http" + "sync/atomic" +) + +// HTTPProviderHandler is an http.Handler whose target can be set after the +// libp2p host is constructed. We need this because the WebSocket transport +// option is wired into the host at FX init time (before *core.IpfsNode and +// the trustless gateway handler exist), but the handler we want to expose +// behind the AutoTLS cert is only available later, once IpfsNode is up. +// +// Used by the HTTPProvider feature that exposes the trustless gateway +// (NoFetch, raw blocks via ?format=raw only) on the same TCP port as the +// /tls/ws AutoWSS listener. See +// `cmd/ipfs/kubo/daemon.go:serveHTTPProviderOverLibp2p` for the install site. +// +// Until Set is called, the handler responds 503 Service Unavailable, which +// is the correct signal for the small race between Accept and handler-install +// during daemon startup. +type HTTPProviderHandler struct { + target atomic.Pointer[http.Handler] +} + +// NewHTTPProviderHandler returns an empty handler. Call Set once the real +// handler is built. +func NewHTTPProviderHandler() *HTTPProviderHandler { + return &HTTPProviderHandler{} +} + +// Set installs the handler that should serve incoming non-WebSocket requests. +// Safe to call concurrently with ServeHTTP. +func (l *HTTPProviderHandler) Set(h http.Handler) { + l.target.Store(&h) +} + +// ServeHTTP forwards to the installed handler, or returns 503 if none has been +// installed yet. +func (l *HTTPProviderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h := l.target.Load(); h != nil { + (*h).ServeHTTP(w, r) + return + } + http.Error(w, "gateway not ready yet", http.StatusServiceUnavailable) +} + +// RequireHTTP2OverTLS wraps an http.Handler so that requests arriving over +// TLS with HTTP/1.1 are rejected with 426 Upgrade Required. Cleartext +// requests pass through regardless of HTTP version, so reverse-proxy +// deployments that forward HTTP/1.1 to the backend keep working. +// +// Why this lives in kubo and not in the go-libp2p WS transport: the +// transport's WithHTTPHandler is intentionally interop-maximal +// (accepts every HTTP version on every listener) and leaves application +// policy to the caller. The HTTP-over-TLS path here is the public-facing +// AutoTLS endpoint we want to keep looking like a modern HTTPS server +// (multiplexing for bitswap-httpnet, smaller fingerprint surface for +// censors). The plain /ws path stays permissive so reverse proxies that +// only speak HTTP/1.1 to the backend keep working. +// +// HTTP/2 is detected via r.ProtoMajor; TLS via r.TLS != nil. Both fields +// are populated by net/http before the handler runs. +func RequireHTTP2OverTLS(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS != nil && r.ProtoMajor < 2 { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "h2,websocket") + http.Error(w, "this endpoint requires HTTP/2 over TLS; HTTP/1.1 is reserved for the WebSocket upgrade", http.StatusUpgradeRequired) + return + } + h.ServeHTTP(w, r) + }) +} diff --git a/core/node/libp2p/httpprovider_test.go b/core/node/libp2p/httpprovider_test.go new file mode 100644 index 00000000000..d62fc6f226b --- /dev/null +++ b/core/node/libp2p/httpprovider_test.go @@ -0,0 +1,102 @@ +package libp2p + +import ( + "crypto/tls" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHTTPProviderHandler_Unset(t *testing.T) { + fb := NewHTTPProviderHandler() + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + fb.ServeHTTP(rec, req) + + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("want 503 before Set, got %d", rec.Code) + } +} + +func TestHTTPProviderHandler_Set(t *testing.T) { + fb := NewHTTPProviderHandler() + fb.Set(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusTeapot) + _, _ = io.WriteString(w, "served") + })) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/x", nil) + fb.ServeHTTP(rec, req) + + if rec.Code != http.StatusTeapot { + t.Fatalf("want 418 after Set, got %d", rec.Code) + } + if got := rec.Body.String(); got != "served" { + t.Fatalf("unexpected body: %q", got) + } +} + +// TestHTTPProviderHandler_Reset confirms Set replaces the previous handler; +// useful in case the gateway needs to be swapped during reconfiguration. +func TestHTTPProviderHandler_Reset(t *testing.T) { + fb := NewHTTPProviderHandler() + fb.Set(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) })) + fb.Set(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(500) })) + + rec := httptest.NewRecorder() + fb.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + if rec.Code != 500 { + t.Fatalf("want second handler (500), got %d", rec.Code) + } +} + +// TestRequireHTTP2OverTLS exercises the four (TLS yes/no ร— h1/h2) cases. +func TestRequireHTTP2OverTLS(t *testing.T) { + hits := 0 + wrapped := RequireHTTP2OverTLS(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + hits++ + w.WriteHeader(http.StatusOK) + })) + + tlsState := &tls.ConnectionState{} + + cases := []struct { + name string + tls *tls.ConnectionState + protoMajor int + wantStatus int + wantHits int + }{ + {"TLS_h1_rejected", tlsState, 1, http.StatusUpgradeRequired, 0}, + {"TLS_h2_allowed", tlsState, 2, http.StatusOK, 1}, + {"cleartext_h1_allowed", nil, 1, http.StatusOK, 1}, + {"cleartext_h2_allowed", nil, 2, http.StatusOK, 1}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + hits = 0 + req := httptest.NewRequest(http.MethodGet, "/x", nil) + req.TLS = c.tls + req.ProtoMajor = c.protoMajor + + rec := httptest.NewRecorder() + wrapped.ServeHTTP(rec, req) + + if rec.Code != c.wantStatus { + t.Fatalf("status: want %d, got %d", c.wantStatus, rec.Code) + } + if hits != c.wantHits { + t.Fatalf("handler invocations: want %d, got %d", c.wantHits, hits) + } + if c.wantStatus == http.StatusUpgradeRequired { + if got := rec.Header().Get("Upgrade"); got != "h2,websocket" { + t.Fatalf("Upgrade header: want %q, got %q", "h2,websocket", got) + } + } + }) + } +} diff --git a/core/node/libp2p/transport.go b/core/node/libp2p/transport.go index 62ff907043b..026546c77d2 100644 --- a/core/node/libp2p/transport.go +++ b/core/node/libp2p/transport.go @@ -2,7 +2,9 @@ package libp2p import ( "fmt" + "net/http" "os" + "time" "github.com/ipfs/kubo/config" "github.com/ipshipyard/p2p-forge/client" @@ -17,11 +19,44 @@ import ( "go.uber.org/fx" ) -func Transports(tptConfig config.Transports) any { +// HTTPProvider HTTP server tuning. The trustless gateway streams large +// block/CAR responses, so it sets only streaming-safe timeouts; Transports +// applies each value. +const ( + // httpProviderMaxConcurrentStreams caps parallel HTTP/2 streams per client + // connection. Bitswap over HTTP (httpnet) opens one connection per peer and + // multiplexes block requests as separate streams, so this doubles as the + // per-peer in-flight block ceiling. Go's HTTP/2 server defaults to 250; 256 + // keeps a power-of-two of headroom so a busy client can saturate parallel + // fetches while bounding per-connection memory. This is a per-connection + // transport limit; Gateway.MaxConcurrentRequests caps total in-flight + // requests across all connections at the application layer (429 once + // exceeded). + httpProviderMaxConcurrentStreams = 256 + + // httpProviderReadHeaderTimeout guards HTTP/1.1 fallback connections + // against slow-header clients. Go's HTTP/2 server ignores it. + httpProviderReadHeaderTimeout = 10 * time.Second + + // httpProviderIdleTimeout caps idle pooled connections. It exceeds + // httpnet's 30s IdleConnTimeout so the client closes idle connections + // first. + httpProviderIdleTimeout = 60 * time.Second + + // httpProviderConnGuardMargin pads Gateway.RetrievalTimeout to derive the + // HTTP/2 WriteByteTimeout and SendPingTimeout, keeping both guards above + // the gateway's own timeout so the gateway returns a clean 504 (with + // diagnostics and a recorded metric) before the connection drops. + httpProviderConnGuardMargin = 30 * time.Second +) + +func Transports(tptConfig config.Transports, gatewayRetrievalTimeout time.Duration) any { return func(params struct { fx.In - Fprint PNetFingerprint `optional:"true"` - ForgeMgr *client.P2PForgeCertMgr `optional:"true"` + Fprint PNetFingerprint `optional:"true"` + ForgeMgr *client.P2PForgeCertMgr `optional:"true"` + HTTPProvider *HTTPProviderHandler `optional:"true"` + SelfSignedTLS *SelfSignedTestTLSConfig `optional:"true"` }, ) (opts Libp2pOpts, err error) { privateNetworkEnabled := params.Fprint != nil @@ -34,11 +69,44 @@ func Transports(tptConfig config.Transports) any { } if wsEnabled { - if params.ForgeMgr == nil { - opts.Opts = append(opts.Opts, libp2p.Transport(websocket.New)) - } else { - opts.Opts = append(opts.Opts, libp2p.Transport(websocket.New, websocket.WithTLSConfig(params.ForgeMgr.TLSConfig()))) + var wsOpts []any + // Test escape hatch wins when set: skip the AutoTLS pipeline + // and feed the WebSocket transport an in-memory self-signed + // cert. Production paths use ForgeMgr; both are wired + // optional so only one provider fires per build. + switch { + case params.SelfSignedTLS != nil: + wsOpts = append(wsOpts, websocket.WithTLSConfig(params.SelfSignedTLS.Config)) + case params.ForgeMgr != nil: + wsOpts = append(wsOpts, websocket.WithTLSConfig(params.ForgeMgr.TLSConfig())) + } + // HTTPProvider: when the master switch is on (and AutoTLS is on), + // expose the trustless gateway handler on the same TCP port as + // /tls/ws by routing non-WebSocket requests to a fallback handler. + // The handler itself is wired post-construction by daemon.go + // because it needs the fully constructed *core.IpfsNode. + // See HTTPProviderHandler. + if params.HTTPProvider != nil { + wsOpts = append(wsOpts, websocket.WithHTTPHandler(params.HTTPProvider)) + // WriteByteTimeout resets on every byte written, so it closes a + // stalled writer (a client that stopped reading) without + // truncating a healthy slow download. SendPingTimeout reclaims + // dead h2 connections, freeing the resource manager's + // connection budget. WriteTimeout and ReadTimeout stay unset: + // they cap the whole request and would truncate a large + // download. See the const block above for the rest. + connGuard := gatewayRetrievalTimeout + httpProviderConnGuardMargin + wsOpts = append(wsOpts, websocket.WithHTTPServerConfig(func(s *http.Server) { + s.ReadHeaderTimeout = httpProviderReadHeaderTimeout + s.IdleTimeout = httpProviderIdleTimeout + s.HTTP2 = &http.HTTP2Config{ + MaxConcurrentStreams: httpProviderMaxConcurrentStreams, + WriteByteTimeout: connGuard, + SendPingTimeout: connGuard, + } + })) } + opts.Opts = append(opts.Opts, libp2p.Transport(websocket.New, wsOpts...)) } if tcpEnabled && wsEnabled && os.Getenv("LIBP2P_TCP_MUX") != "false" { diff --git a/docs/changelogs/v0.44.md b/docs/changelogs/v0.44.md new file mode 100644 index 00000000000..57256059a7b --- /dev/null +++ b/docs/changelogs/v0.44.md @@ -0,0 +1,33 @@ +# Kubo changelog v0.44 + + + +This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. + +- [v0.44.0](#v0440) + +## v0.44.0 + +- [Overview](#overview) +- [๐Ÿ”ฆ Highlights](#-highlights) + - [๐ŸŒ Experimental `HTTPProvider`: serve local data over plain HTTP/2](#-experimental-httpprovider-serve-local-data-over-plain-http2) +- [๐Ÿ“ Changelog](#-changelog) +- [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors) + +### Overview + +### ๐Ÿ”ฆ Highlights + +#### ๐ŸŒ Experimental `HTTPProvider`: serve local data over plain HTTP/2 + +Kubo now ships an experimental, opt-in `HTTPProvider`: the **server** side of HTTP retrieval. Enable it on a publicly diallable node and the local trustless gateway becomes reachable over plain HTTP/2 alongside the existing libp2p path, on the same swarm port (`4001` by default). Kubo reuses the certificate it already obtains through [`AutoTLS`](https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls), so no extra TLS setup is required. + +`HTTPProvider` is read-only: it serves raw blocks (`?format=raw`) from the local blockstore and does not fetch missing data from the network. It is **not** the same as [`Gateway`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gateway), which stays on loopback `127.0.0.1:8080` and is the recursive, deserializing interface for local browsing. + +This is a meaningful step toward broader IPFS interoperability: a stock HTTP client, a browser, `curl`, or any HTTP library can fetch verifiable blocks from a Kubo node without a libp2p stack. It pairs with the existing [`HTTPRetrieval`](https://github.com/ipfs/kubo/blob/master/docs/config.md#httpretrieval) (the client side). + +Off by default. See [`HTTPProvider`](https://github.com/ipfs/kubo/blob/master/docs/config.md#httpprovider) for the available settings and how to turn it on. + +### ๐Ÿ“ Changelog + +### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors diff --git a/docs/config.md b/docs/config.md index 06539eab4dc..fe09aa05657 100644 --- a/docs/config.md +++ b/docs/config.md @@ -230,6 +230,11 @@ config file at runtime. - [`DNS`](#dns) - [`DNS.Resolvers`](#dnsresolvers) - [`DNS.MaxCacheTTL`](#dnsmaxcachettl) + - [`HTTPProvider`](#httpprovider) + - [`HTTPProvider.Enabled`](#httpproviderenabled) + - [`HTTPProvider.Libp2p`](#httpproviderlibp2p) + - [`HTTPProvider.Cleartext`](#httpprovidercleartext) + - [`HTTPProvider.AnnounceMultiaddrs`](#httpproviderannouncemultiaddrs) - [`HTTPRetrieval`](#httpretrieval) - [`HTTPRetrieval.Enabled`](#httpretrievalenabled) - [`HTTPRetrieval.Allowlist`](#httpretrievalallowlist) @@ -836,6 +841,18 @@ Do not change this unless you self-host [p2p-forge] under own domain. > (proving ownership of PeerID), probes if your Kubo node can correctly answer to a [libp2p Identify](https://github.com/libp2p/specs/tree/master/identify) query. > This ensures only a correctly configured, publicly dialable Kubo can initiate [ACME DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) for `peerid.libp2p.direct`. +> [!TIP] +> Two optional query parameters help with local debugging and end-to-end tests. Kubo strips them before handing the URL to [p2p-forge] and preserves any unrelated query parameters. Both expect a literal `host:port`; bracket IPv6 hosts, e.g. `[::1]:9000`. +> +> - `?dial=host:port` sends the registration request to `host:port` while keeping the URL Kubo advertises (and signs over for PeerID-auth) unchanged. This lets a local [p2p-forge] instance on a loopback port answer requests whose `Host` header must still match the production registration hostname. +> - `?dns=host:port` points the DNS-01 propagation pre-flight check at this DNS server instead of the system resolver. Needed when the forge's domain is not in public DNS (e.g. a private `.test` suffix used in tests). +> +> Both overrides apply only to the forge registration request and its DNS-01 pre-flight check. They do not affect `AutoTLS.CAEndpoint` or any other URL. Example combining both: +> +> ``` +> http://registration.libp2p.test/?dial=127.0.0.1:42013&dns=127.0.0.1:5353 +> ``` + Default: `https://registration.libp2p.direct` (public good run by [Interplanetary Shipyard](https://ipshipyard.com)) Type: `optionalString` @@ -1340,6 +1357,8 @@ Type: `flag` Maximum duration Kubo will wait for content retrieval (new bytes to arrive). +This covers any wait for new bytes, including reads from the local datastore, not only network retrieval from other peers. If your datastore is not truly local, for example backed by NFS or an object store like S3, slow reads from it can also trip this timeout. Raise the value when the backend can be slower than the default allows. + **Timeout behavior:** - **Time to first byte**: Returns 504 Gateway Timeout if the gateway cannot start writing within this duration (e.g., stuck searching for providers) @@ -3946,11 +3965,77 @@ Default: Respect DNS Response TTL Type: `optionalDuration` +## `HTTPProvider` + +`HTTPProvider` is the **server** side of HTTP retrieval in Kubo. It exposes the local trustless gateway (in [`NoFetch`](#gatewaynofetch) mode) over plain HTTP/2 so any standard HTTP client (a browser, `curl`, or any HTTP library) can fetch verifiable blocks from this node without a libp2p stack. The matching **client** side lives under [`HTTPRetrieval`](#httpretrieval). + +### Mental model: `HTTPProvider` vs `Gateway` + +`HTTPProvider` and [`Gateway`](#gateway) serve different audiences with different rules. Keep them straight: + +- `HTTPProvider` serves **raw data to the network**. It runs on Kubo's swarm port (`4001` by default), reads only from the local blockstore, and returns only raw blocks (`?format=raw`). It does **not** trigger network retrieval for missing data, and it does **not** deserialize content. Other peers fetch from it. +- [`Gateway`](#gateway) serves **deserialized data to the local user**. It runs on a loopback port (`127.0.0.1:8080` by default), walks DAGs, deserializes UnixFS, follows redirects, and fetches missing data from the network. You (or a local app on your machine) browse with it. + +The split is intentional. A public-facing HTTP source on the swarm port should not deserialize content for unknown clients or recurse out to the network on their behalf. A recursive browser endpoint should stay on loopback, not be exposed to the open internet. + +### How the HTTP listener is wired up + +- The HTTP listener reuses the same `/tcp` socket as the `/ws` listener through Kubo's shared-TCP demuxer. No extra port is opened. +- Today the HTTP listener requires a WebSocket listener on the same TCP port to exist. When `/ws` is present, Kubo adds the matching `/http` multiaddr to the announced address set automatically. +- When [`AutoTLS.AutoWSS`](#autotlsautowss) is enabled, Kubo also adds a `/tls/http` listener on the same host and port as the `/tls/ws` listener, reusing the same AutoTLS certificate. No separate TLS setup is required. + +> [!IMPORTANT] +> Experimental and off by default. Enable it on publicly diallable nodes where you want plain-HTTP clients to fetch blocks directly. + +Default: `{}` + +Type: `object` + +### `HTTPProvider.Enabled` + +Master switch for `HTTPProvider`. When `true`, Kubo registers the trustless gateway handler and defaults [`HTTPProvider.Libp2p`](#httpproviderlibp2p) to `true`. The [`HTTPProvider.Cleartext`](#httpprovidercleartext) and [`HTTPProvider.AnnounceMultiaddrs`](#httpproviderannouncemultiaddrs) sub-toggles stay off until set explicitly. + +Default: `false` + +Type: `flag` + +### `HTTPProvider.Libp2p` + +Exposes the trustless gateway over a libp2p stream, as specified by the [libp2p Gateway spec](https://specs.ipfs.tech/http-gateways/libp2p-gateway/). The handler is mounted under the `/ipfs/gateway` protocol ID and advertised via `.well-known/libp2p/protocols`. + +Default: `true` when `HTTPProvider.Enabled=true` + +Type: `flag` + +### `HTTPProvider.Cleartext` + +Auto-appends a plaintext `/ws` listener to each `/tcp/N` already in [`Addresses.Swarm`](#addressesswarm). The new `/ws` shares the existing TCP port via the shared-TCP demuxer, so no extra socket is opened. + +Intended for deployments where the operator handles TLS termination upstream: a reverse proxy such as Caddy, Traefik, or nginx sits in front of Kubo and forwards either HTTP/1.1 or HTTP/2 cleartext to this node. With [`AutoTLS`](#autotls), Kubo already serves `/tls/ws` and `/tls/http` directly with a real certificate, so a cleartext path is unnecessary and would expose the trustless gateway and WebSocket upgrade unencrypted on the public network. Flip it on knowingly. + +The corresponding `/http` announcement is controlled by [`HTTPProvider.AnnounceMultiaddrs`](#httpproviderannouncemultiaddrs). + +Default: `false` + +Type: `flag` + +### `HTTPProvider.AnnounceMultiaddrs` + +Derives an HTTP-flavored multiaddr from each WebSocket listener on this peer and includes it in the announced address set: `/ws` becomes `/http`, `/tls/ws` becomes `/tls/http`, `/tls/sni//ws` becomes `/tls/sni//http`. The HTTP endpoint shares the same TCP port and TLS certificate as the WebSocket listener, so this is purely an announcement; no extra socket is opened. + +This is what lets [`HTTPRetrieval`](#httpretrieval) clients discover this peer as an HTTP source through identify, the DHT, and IPNI without out-of-band knowledge. + +Subject to [`Addresses.NoAnnounce`](#addressesnoannounce) filters, like any other announced multiaddr. + +Off by default even when [`HTTPProvider.Enabled`](#httpproviderenabled) is `true`: turning the gateway handler on does not automatically broadcast this node as an HTTP source. Flip this on once you are ready to advertise. + +Default: `false` + +Type: `flag` + ## `HTTPRetrieval` -`HTTPRetrieval` is configuration for pure HTTP retrieval based on Trustless HTTP Gateways' -[Block Responses (`application/vnd.ipld.raw`)](https://specs.ipfs.tech/http-gateways/trustless-gateway/#block-responses-application-vnd-ipld-raw) -which can be used in addition to or instead of retrieving blocks with [Bitswap over Libp2p](#bitswap). +`HTTPRetrieval` is the **client** side of HTTP retrieval in Kubo. It lets the node fetch blocks from peers that advertise an HTTP gateway endpoint, using pure HTTP requests based on the Trustless Gateway [Block Responses (`application/vnd.ipld.raw`)](https://specs.ipfs.tech/http-gateways/trustless-gateway/#block-responses-application-vnd-ipld-raw) spec. HTTP fetches can run in addition to or instead of [Bitswap over Libp2p](#bitswap). The matching **server** side lives under [`HTTPProvider`](#httpprovider). Default: `{}` diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 0a3a9217179..0199e6796b8 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -9,7 +9,7 @@ replace github.com/ipfs/kubo => ./../../.. require ( github.com/ipfs/boxo v0.41.0 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 - github.com/libp2p/go-libp2p v0.48.0 + github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 github.com/multiformats/go-multiaddr v0.16.1 ) diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 7dd9ff5ac1d..6305c756c46 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -411,8 +411,8 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= -github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 h1:Y1LIBzKrtU/NbptugPNuem74f355oyWdLRIm0edgozo= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= diff --git a/docs/experimental-features.md b/docs/experimental-features.md index abd45ff3ca5..51c59f2a811 100644 --- a/docs/experimental-features.md +++ b/docs/experimental-features.md @@ -471,7 +471,7 @@ Replaced by [`Provide.Enabled`](https://github.com/ipfs/kubo/blob/master/docs/co Removed, no plans to reintegrate either as experimental or stable feature. -[Trustless Gateway over Libp2p](#http-gateway-over-libp2p) should be easier to use for unixfs usecases and support basic wildcard car streams for non unixfs. +The libp2p-stream transport in [`HTTPProvider.Libp2p`](https://github.com/ipfs/kubo/blob/master/docs/config.md#httpproviderlibp2p) is easier to use for UnixFS use cases and supports basic wildcard CAR streams for non-UnixFS. See https://github.com/ipfs/kubo/pull/9747 for more information. @@ -566,49 +566,15 @@ ipfs config --json Experimental.OptimisticProvideJobsPoolSize 120 ## HTTP Gateway over Libp2p -### In Version - -0.23.0 - -### State - -Experimental, disabled by default. - -Enables serving a subset of the [IPFS HTTP Gateway](https://specs.ipfs.tech/http-gateways/) semantics over libp2p `/http/1.1` protocol. - -Notes: -- This feature only about serving verifiable gateway requests over libp2p: - - Deserialized responses are not supported. - - Only operate on `/ipfs` resources (no `/ipns` atm) - - Only support requests for `application/vnd.ipld.raw` and - `application/vnd.ipld.car` (from [Trustless Gateway Specification](https://specs.ipfs.tech/http-gateways/trustless-gateway/), - where data integrity can be verified). - - Only serve data that is already local to the node (i.e. similar to a - [`Gateway.NoFetch`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewaynofetch)) -- While Kubo currently mounts the gateway API at the root (i.e. `/`) of the - libp2p `/http/1.1` protocol, that is subject to change. - - The way to reliably discover where a given HTTP protocol is mounted on a - libp2p endpoint is via the `.well-known/libp2p` resource specified in the - [http+libp2p specification](https://github.com/libp2p/specs/pull/508) - - The identifier of the protocol mount point under `/http/1.1` listener is - `/ipfs/gateway`, as noted in - [ipfs/specs#434](https://github.com/ipfs/specs/pull/434). +This feature has moved out of `Experimental` and now lives at [`HTTPProvider.Libp2p`](https://github.com/ipfs/kubo/blob/master/docs/config.md#httpproviderlibp2p), the libp2p-stream transport of the broader [`HTTPProvider`](https://github.com/ipfs/kubo/blob/master/docs/config.md#httpprovider) feature. -### How to enable - -Modify your ipfs config: +The previous `Experimental.GatewayOverLibp2p` setting has been removed. Kubo now refuses to start when it is set; replace it with: ``` -ipfs config --json Experimental.GatewayOverLibp2p true +ipfs config --json HTTPProvider.Enabled true +ipfs config --json HTTPProvider.Libp2p true ``` -### Road to being a real feature - -- [ ] Needs more people to use and report on how well it works -- [ ] Needs UX work for exposing non-recursive "HTTP transport" (NoFetch) over both libp2p and plain TCP (and sharing the configuration) -- [ ] Needs a mechanism for HTTP handler to signal supported features ([IPIP-425](https://github.com/ipfs/specs/pull/425)) -- [ ] Needs an option for Kubo to detect peers that have it enabled and prefer HTTP transport before falling back to bitswap (and use CAR if peer supports dag-scope=entity from [IPIP-402](https://specs.ipfs.tech/ipips/ipip-0402/)) - ## Accelerated DHT Client This feature now lives at [`Routing.AcceleratedDHTClient`](https://github.com/ipfs/kubo/blob/master/docs/config.md#routingaccelerateddhtclient). diff --git a/go.mod b/go.mod index 0192b2e21d9..ba5090778aa 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 github.com/fsnotify/fsnotify v1.10.1 github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/hanwen/go-fuse/v2 v2.10.1 github.com/hashicorp/go-version v1.9.0 github.com/ipfs-shipyard/nopfs v0.0.14 @@ -50,7 +51,7 @@ require ( github.com/jbenet/go-temp-err-catcher v0.1.0 github.com/julienschmidt/httprouter v1.3.0 github.com/libp2p/go-doh-resolver v0.5.0 - github.com/libp2p/go-libp2p v0.48.0 + github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 github.com/libp2p/go-libp2p-http v0.5.0 github.com/libp2p/go-libp2p-kad-dht v0.40.0 github.com/libp2p/go-libp2p-kbucket v0.8.0 @@ -91,6 +92,7 @@ require ( golang.org/x/crypto v0.52.0 golang.org/x/exp v0.0.0-20260603202125-055de637280b golang.org/x/mod v0.36.0 + golang.org/x/net v0.55.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/term v0.43.0 @@ -147,7 +149,6 @@ require ( github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect github.com/google/gopacket v1.1.19 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/guillaumemichel/reservedpool v0.3.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -257,7 +258,6 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect golang.org/x/text v0.37.0 // indirect diff --git a/go.sum b/go.sum index ae785adb8df..3ab21a2d625 100644 --- a/go.sum +++ b/go.sum @@ -494,8 +494,8 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= -github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 h1:Y1LIBzKrtU/NbptugPNuem74f355oyWdLRIm0edgozo= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= diff --git a/mk/golang.mk b/mk/golang.mk index 0551cb78622..2efae123d4a 100644 --- a/mk/golang.mk +++ b/mk/golang.mk @@ -101,6 +101,16 @@ test_examples: cd docs/examples/kubo-as-a-library && go test -v -timeout=$(TEST_EXAMPLES_TIMEOUT) ./... && cp go.mod go.mod.bak && cp go.sum go.sum.bak && (go mod edit -replace github.com/ipfs/kubo=./../../.. && go mod tidy && go test -v -timeout=$(TEST_EXAMPLES_TIMEOUT) ./...; ret=$$?; mv go.mod.bak go.mod; mv go.sum.bak go.sum; exit $$ret) .PHONY: test_examples +# AutoTLS end-to-end canary (separate module under test/autotls). +# Spins up an in-process Pebble ACME server and the p2p-forge service +# (CoreDNS-based) to exercise kubo's full AutoTLS chain. Heavy deps +# (Pebble + CoreDNS, ~350 indirect modules) live in the sub-module's +# go.mod so kubo's main module stays clean. +TEST_AUTOTLS_TIMEOUT ?= 4m +test_autotls: cmd/ipfs/ipfs test/bin/gotestsum $$(DEPS_GO) + cd test/autotls && rm -f autotls-tests.json && PATH="$(CURDIR)/cmd/ipfs:$(CURDIR)/test/bin:$$PATH" gotestsum $(GOTESTSUM_NOCOLOR) --jsonfile autotls-tests.json -- -v -timeout=$(TEST_AUTOTLS_TIMEOUT) ./... +.PHONY: test_autotls + # Build kubo for all platforms from .github/build-platforms.yml test_go_build: bin/test-go-build-platforms diff --git a/test/autotls/canary_test.go b/test/autotls/canary_test.go new file mode 100644 index 00000000000..70668a8df3f --- /dev/null +++ b/test/autotls/canary_test.go @@ -0,0 +1,225 @@ +package autotls + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ipfs/go-cid" + "github.com/ipfs/kubo/config" + "github.com/ipfs/kubo/test/cli/harness" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + "golang.org/x/net/http2" +) + +// TestACMEEndToEnd drives kubo's full AutoTLS chain in-process: +// +// 1. p2p-forge registration (PeerID-auth POST) +// 2. certmagic ACME order against an in-process Pebble +// 3. DNS-01 challenge validated through the p2p-forge DNS plugin +// 4. cert installation into the WebSocket transport +// 5. HTTP/2 fetch of a real block via the announced /tls/http multiaddr +// +// Every link above is load-bearing for any AutoTLS-enabled kubo node, so one +// canary covers all of them. Heavy deps (Pebble, CoreDNS) live in this +// sub-module's go.mod and stay out of kubo's main module; CI runs the canary +// as a dedicated autotls-tests job. See doc.go. +func TestACMEEndToEnd(t *testing.T) { + t.Parallel() + + stack := NewStack(t) + + // The kubo harness finds the binary by walking up to the nearest + // go.mod, but this sub-module has its own go.mod, so the walk stops + // here. Point IPFSBin at the real kubo build at the repo root + // (../../cmd/ipfs/ipfs from this sub-module's perspective). + h := harness.NewT(t) + h.IPFSBin = mustAbs(t, "../../cmd/ipfs/ipfs") + node := h.NewNode().Init() + node.UpdateConfig(func(cfg *config.Config) { + // HTTPProvider exposes the trustless gateway over /tls/ws and + // /tls/http using the AutoTLS cert. The canary waits for the + // /tls/http multiaddr in identify output, so enable the + // announcement. + cfg.HTTPProvider.Enabled = config.True + cfg.HTTPProvider.AnnounceMultiaddrs = config.True + + // Point AutoTLS at the in-process Pebble + p2p-forge. + cfg.AutoTLS.Enabled = config.True + cfg.AutoTLS.DomainSuffix = config.NewOptionalString(stack.ForgeDomain) + cfg.AutoTLS.RegistrationEndpoint = config.NewOptionalString(stack.ForgeRegistrationEndpoint) + cfg.AutoTLS.RegistrationToken = config.NewOptionalString(stack.ForgeAuthToken) + cfg.AutoTLS.CAEndpoint = config.NewOptionalString(stack.ACMEEndpoint) + cfg.AutoTLS.TrustedCARootsPEM = config.NewOptionalString(stack.PebbleCAPEM) + cfg.AutoTLS.AllowPrivateForgeAddrs = config.True + // Skip the production registration-delay so the test doesn't + // have to wait for the libp2p reachability event. + cfg.AutoTLS.RegistrationDelay = config.NewOptionalDuration(0) + + // AutoWSS adds /tls/sni/*.libp2p.test/ws to each /tcp/N, which + // is what HTTPProvider's /tls/http announcement is derived from. + cfg.AutoTLS.AutoWSS = config.True + }) + + node.StartDaemon() + defer node.StopDaemon() + + // Wait for AutoTLS to obtain a cert and the resulting /tls/http + // multiaddr to show up in the announced address set. + defer func() { + if t.Failed() { + t.Logf("daemon stderr (tail):\n%s", tailString(node.Daemon.Stderr.String(), 20000)) + } + }() + announced := waitForTLSHTTPMultiaddr(t, node, 60*time.Second, stack.ForgeDomain) + + // Translate the /tls/http multiaddr into an HTTPS URL and a dial + // address (raw IP:port we actually connect to; SNI carries the + // forge-issued hostname). + hostname, port := splitTLSHTTPAddr(t, announced) + addInfo := node.IPFSAddStr("hello world") + addCID := cid.MustParse(addInfo) + url := fmt.Sprintf("https://%s:%s/ipfs/%s?format=raw", hostname, port, addCID) + + // Build an HTTP/2 client that trusts Pebble's ACME issuance root. + // Cert chain validation goes all the way: Pebble-signed end-entity + // cert, chained from Pebble's issuance root, served from a hostname + // under stack.ForgeDomain. + pool := x509.NewCertPool() + require.True(t, pool.AppendCertsFromPEM([]byte(stack.PebbleIssuanceRootPEM)), + "failed to parse Pebble issuance root PEM") + client := &http.Client{ + Transport: &http2.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + ServerName: hostname, + }, + DialTLS: func(network, _ string, cfg *tls.Config) (net.Conn, error) { + raw, err := net.Dial(network, net.JoinHostPort("127.0.0.1", port)) + if err != nil { + return nil, err + } + c := tls.Client(raw, cfg) + if err := c.Handshake(); err != nil { + _ = raw.Close() + return nil, err + } + return c, nil + }, + }, + Timeout: 15 * time.Second, + } + + resp, err := client.Get(url) + require.NoError(t, err, "fetch /ipfs/%s via %s", addCID, url) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "HTTP/2.0", resp.Proto) + require.Equal(t, "application/vnd.ipld.raw", resp.Header.Get("Content-Type")) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + // `ipfs add` of a small string produces a UnixFS file, so the raw + // block is a UnixFS protobuf with the literal payload embedded. We + // don't unmarshal it here; the canary's job is to prove the AutoTLS + // path round-trips real block bytes, not to re-test UnixFS. + require.Contains(t, string(body), "hello world", + "raw block payload should embed the added bytes") +} + +// waitForTLSHTTPMultiaddr polls `ipfs id` until a multiaddr that ends in +// /tls/http and references the forge domain appears, or the deadline +// expires. Pebble's cert issuance takes a second or two on the first run; +// the AddrsFactory derivation of /tls/http happens once the AutoTLS cert +// is installed and the libp2p host's listen addresses are re-evaluated. +func waitForTLSHTTPMultiaddr(t *testing.T, n *harness.Node, timeout time.Duration, forgeDomain string) multiaddr.Multiaddr { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + for _, a := range readAnnouncedAddrs(t, n) { + s := a.String() + if !strings.HasSuffix(s, "/tls/http") { + continue + } + if !strings.Contains(s, forgeDomain) { + continue + } + return a + } + time.Sleep(500 * time.Millisecond) + } + t.Fatalf("no /tls/http multiaddr appeared within %s; announced=%v", + timeout, readAnnouncedAddrs(t, n)) + return nil +} + +// readAnnouncedAddrs returns the multiaddrs the node advertises via +// identify (output of `ipfs id`). This is what other peers see, and +// includes the /tls/http addresses derived by HTTPProvider. +func readAnnouncedAddrs(t *testing.T, n *harness.Node) []multiaddr.Multiaddr { + t.Helper() + out := n.IPFS("id", "--enc=json").Stdout.String() + var idDoc struct { + Addresses []string `json:"Addresses"` + } + require.NoError(t, json.Unmarshal([]byte(out), &idDoc)) + addrs := make([]multiaddr.Multiaddr, 0, len(idDoc.Addresses)) + for _, s := range idDoc.Addresses { + if i := strings.Index(s, "/p2p/"); i >= 0 { + s = s[:i] + } + a, err := multiaddr.NewMultiaddr(s) + if err != nil { + t.Fatalf("parse announced addr %q: %v", s, err) + } + addrs = append(addrs, a) + } + return addrs +} + +// splitTLSHTTPAddr extracts (hostname, port) from a /dns*//tcp// +// tls/sni//http or similar multiaddr. The hostname is what SNI must +// be set to; the port is what we actually dial on 127.0.0.1. +func splitTLSHTTPAddr(t *testing.T, m multiaddr.Multiaddr) (host, port string) { + t.Helper() + // SNI component (if present) wins for the hostname, otherwise the + // /dns* value. Both forms appear depending on AutoTLS.ShortAddrs. + for _, code := range []int{multiaddr.P_SNI, multiaddr.P_DNS, multiaddr.P_DNS4, multiaddr.P_DNS6, multiaddr.P_DNSADDR} { + v, err := m.ValueForProtocol(code) + if err == nil && v != "" { + host = v + break + } + } + require.NotEmpty(t, host, "no hostname component in %s", m) + p, err := m.ValueForProtocol(multiaddr.P_TCP) + require.NoError(t, err, "no tcp port in %s", m) + return host, p +} + +// mustAbs returns the absolute path of rel, panicking via require if the +// filesystem call fails. Used to compute the kubo binary location from +// this sub-module's cwd. +func mustAbs(t *testing.T, rel string) string { + t.Helper() + abs, err := filepath.Abs(rel) + require.NoError(t, err) + return abs +} + +// tailString returns the last n bytes of s, prefixed with "...". +func tailString(s string, n int) string { + if len(s) <= n { + return s + } + return "... (truncated)\n" + s[len(s)-n:] +} diff --git a/test/autotls/doc.go b/test/autotls/doc.go new file mode 100644 index 00000000000..df3f1ac6f34 --- /dev/null +++ b/test/autotls/doc.go @@ -0,0 +1,43 @@ +// Package autotls contains end-to-end tests for the AutoTLS / p2p-forge +// / ACME issuance chain. +// +// # Why a separate Go module +// +// These tests need to run an in-process Pebble (ACME test server) and the +// full p2p-forge service (CoreDNS-based DNS server + HTTP registration +// endpoint). Pulling those into kubo's main go.mod brings ~350 transitive +// dependencies, including AWS, Azure, GCP, DataDog, and Kubernetes SDKs +// (because CoreDNS supports DNS plugins for all of them). None of that +// belongs in the production `ipfs` binary or in the main module's +// vulnerability-scanning surface. +// +// Isolating the heavy test deps behind their own go.mod keeps: +// +// - kubo's main module clean: `go build ./cmd/ipfs/` and `go mod tidy` at +// the repo root are unaffected. +// - Vulnerability scanning focused: Dependabot does not gain ~350 noisy +// advisories about cloud SDKs that production code does not use. +// - `go test ./...` at the repo root fast: this module is not picked up +// by default. Run it via `make test_autotls` from the repo root, +// or directly with `cd test/autotls && go test ./...`. +// +// The pattern matches the existing test sub-module in this repo: +// `test/dependencies/go.mod`. +// +// # CI +// +// This module is exercised by a dedicated GitHub Actions job +// (`autotls-tests` in `.github/workflows/gotest.yml`), parallel to the +// main `test_cli` job. The `test_cli` target intentionally does not invoke +// this module, so the CLI suite's compile time stays unaffected by Pebble +// and CoreDNS. +// +// # TODO: consider promoting test/cli to its own module +// +// A cleaner long-term separation would promote the entire `test/cli` tree +// to its own go.mod (with a `replace github.com/ipfs/kubo => ../..` +// directive). That move would isolate every test-only dependency from the +// production module, not just this canary's heavy deps. The cost is +// touching every existing CLI test file and every Makefile / CI target +// that references `test_cli`. Worth doing as its own focused PR. +package autotls diff --git a/test/autotls/go.mod b/test/autotls/go.mod new file mode 100644 index 00000000000..77a9675788a --- /dev/null +++ b/test/autotls/go.mod @@ -0,0 +1,234 @@ +module github.com/ipfs/kubo/test/autotls + +go 1.26.4 + +// Use the parent kubo tree, not a published version. +replace github.com/ipfs/kubo => ../.. + +require ( + github.com/coredns/caddy v1.1.4 + github.com/coredns/coredns v1.14.3 + github.com/ipfs/go-cid v0.6.1 + github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 + github.com/ipshipyard/p2p-forge v0.9.0 + github.com/letsencrypt/pebble/v2 v2.10.1 + github.com/multiformats/go-multiaddr v0.16.1 + github.com/stretchr/testify v1.11.1 + golang.org/x/net v0.55.0 +) + +require ( + filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect + filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect + github.com/DataDog/zstd v1.5.7 // indirect + github.com/Jorropo/jsync v1.0.1 // indirect + github.com/RaduBerinde/axisds v0.1.0 // indirect + github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.39 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/smithy-go v1.25.1 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/caddyserver/certmagic v0.25.3 // indirect + github.com/caddyserver/zerossl v0.1.5 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble/v2 v2.1.6 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/dgraph-io/badger/v4 v4.5.1 // indirect + github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/filecoin-project/go-clock v0.1.0 // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gaissmai/bart v0.28.0 // indirect + github.com/gammazero/chanqueue v1.1.2 // indirect + github.com/gammazero/deque v1.2.1 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect + github.com/google/flatbuffers v24.12.23+incompatible // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/bbloom v0.1.0 // indirect + github.com/ipfs/boxo v0.41.0 // indirect + github.com/ipfs/go-bitfield v1.1.0 // indirect + github.com/ipfs/go-block-format v0.2.3 // indirect + github.com/ipfs/go-cidutil v0.1.1 // indirect + github.com/ipfs/go-datastore v0.9.1 // indirect + github.com/ipfs/go-ds-badger4 v0.1.8 // indirect + github.com/ipfs/go-ds-dynamodb v0.3.0 // indirect + github.com/ipfs/go-dsqueue v0.2.0 // indirect + github.com/ipfs/go-ipfs-cmds v0.16.1 // indirect + github.com/ipfs/go-ipfs-redirects-file v0.1.2 // indirect + github.com/ipfs/go-ipld-cbor v0.2.1 // indirect + github.com/ipfs/go-ipld-format v0.6.3 // indirect + github.com/ipfs/go-ipld-legacy v0.3.0 // indirect + github.com/ipfs/go-log/v2 v2.9.2 // indirect + github.com/ipfs/go-metrics-interface v0.3.0 // indirect + github.com/ipfs/go-unixfsnode v1.10.4 // indirect + github.com/ipld/go-car/v2 v2.17.0 // indirect + github.com/ipld/go-codec-dagpb v1.7.0 // indirect + github.com/ipld/go-ipld-prime v0.24.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/koron/go-ssdp v0.0.6 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/letsencrypt/challtestsrv v1.4.2 // indirect + github.com/libdns/libdns v1.1.1 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-doh-resolver v0.5.0 // indirect + github.com/libp2p/go-flow-metrics v0.3.0 // indirect + github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.40.0 // indirect + github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect + github.com/libp2p/go-libp2p-record v0.3.1 // indirect + github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/mdlayher/vsock v1.2.1 // indirect + github.com/mholt/acmez/v3 v3.1.6 // indirect + github.com/miekg/dns v1.1.72 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.5.0 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect + github.com/pion/stun/v3 v3.1.1 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/pires/go-proxyproto v0.11.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/polydawn/refmt v0.90.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/exporter-toolkit v0.16.0 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/slok/go-http-metrics v0.13.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect + github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect + github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.45.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + gonum.org/v1/gonum v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.81.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.4.1 // indirect +) diff --git a/test/autotls/go.sum b/test/autotls/go.sum new file mode 100644 index 00000000000..d73d0dd54e8 --- /dev/null +++ b/test/autotls/go.sum @@ -0,0 +1,661 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Jorropo/jsync v1.0.1 h1:6HgRolFZnsdfzRUj+ImB9og1JYOxQoReSywkHOGSaUU= +github.com/Jorropo/jsync v1.0.1/go.mod h1:jCOZj3vrBCri3bSU3ErUYvevKlnbssrXeCivybS5ABQ= +github.com/RaduBerinde/axisds v0.1.0 h1:YItk/RmU5nvlsv/awo2Fjx97Mfpt4JfgtEVAGPrLdz8= +github.com/RaduBerinde/axisds v0.1.0/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 h1:bsU8Tzxr/PNz75ayvCnxKZWEYdLMPDkUgticP4a4Bvk= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54/go.mod h1:0tr7FllbE9gJkHq7CVeeDDFAFKQVy5RnCSSNBOvdqbc= +github.com/aclements/go-perfevent v0.0.0-20240301234650-f7843625020f h1:JjxwchlOepwsUWcQwD2mLUAGE9aCp0/ehy6yCHFBOvo= +github.com/aclements/go-perfevent v0.0.0-20240301234650-f7843625020f/go.mod h1:tMDTce/yLLN/SK8gMOxQfnyeMeCg8KGzp0D1cbECEeo= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.39 h1:Suq0L+BsHm+5QD9bOXTkMTYNqgwhevA2Ggo1ApRqukE= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.20.39/go.mod h1:JX+/lB8RXleUNZ3Uw6o7FnE8t4CkKT8gDHEmlF8qZfI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3 h1:XgjzLEE8CrNYnr4Xmi1W5PfKsKMjp4Pu1rWkJNO43JI= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3/go.mod h1:r7sfLXEN8RUA89tAHy1E7lCtVOOWIkqVy/FbnUdxW1E= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.16 h1:nWMRNW3SFeJCdK7OsZ9lmbl1AAEAs8s6w5Gsa3MQqNo= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.32.16/go.mod h1:IMigEAstzWVC+mYsWLjZB/ZBJBLWON/Fl0zLHxZ18Qk= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.23 h1:3Eo/PBBnjFi1+gYfaL286dpmFSW3mTfodBIybq36Qv4= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.23/go.mod h1:3oh+5xGSd1iuxonVb3Qbm+WJYlbhczT9kbzr6doJLzY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= +github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b h1:SHlYZ/bMx7frnmeqCu+xm0TCxXLzX3jQIVuFbnFGtFU= +github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac= +github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5 h1:UycK/E0TkisVrQbSoxvU827FwgBBcZ95nRRmpj/12QI= +github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5/go.mod h1:jsaKMvD3RBCATk1/jbUZM8C9idWBJME9+VRZ5+Liq1g= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPYtvELQ/h4IrmPAohXqe2pWA8Bwhejr3VQoZsA= +github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895/go.mod h1:aPd7gM9ov9M8v32Yy5NJrDyOcD8z642dqs+F0CeNXfA= +github.com/cockroachdb/pebble/v2 v2.1.6 h1:GDo7Z2+LgFZ7LJLdLmBXhDeTVIwgSPGxIT15hE7vGqM= +github.com/cockroachdb/pebble/v2 v2.1.6/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/coredns/caddy v1.1.4 h1:+Lls5xASB0QsA2jpCroCOwpPlb5GjIGlxdjXxdX0XVo= +github.com/coredns/caddy v1.1.4/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/coredns v1.14.3 h1:hWWoTdONblKIWhC8QPkxLEGIbewhR5xyTedqLVPsvvE= +github.com/coredns/coredns v1.14.3/go.mod h1:15BWsGGxupagKQ3p09pIIZ5kcgmyawquey6gqNWRaEI= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf h1:dwGgBWn84wUS1pVikGiruW+x5XM4amhjaZO20vCjay4= +github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= +github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 h1:BBso6MBKW8ncyZLv37o+KNyy0HrrHgfnOaGQC2qvN+A= +github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5/go.mod h1:JpoxHjuQauoxiFMl1ie8Xc/7TfLuMZ5eOCONd1sUBHg= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gaissmai/bart v0.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko= +github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/gammazero/chanqueue v1.1.2 h1:dZEsxlyANZMyeTRemABqZF8QM9BnE4NBI43Oh3y5fIU= +github.com/gammazero/chanqueue v1.1.2/go.mod h1:XDN1X/jjAbmSceNFOQbtKToeSkxtdVdpKu90LiEdBEE= +github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= +github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e h1:4bw4WeyTYPp0smaXiJZCNnLrvVBqirQVreixayXezGc= +github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/bbloom v0.1.0 h1:nIWwfIE3AaG7RCDQIsrUonGCOTp7qSXzxH7ab/ss964= +github.com/ipfs/bbloom v0.1.0/go.mod h1:lDy3A3i6ndgEW2z1CaRFvDi5/ZTzgM1IxA/pkL7Wgts= +github.com/ipfs/boxo v0.41.0 h1:diKlFosOG2e1mgSO1CXqcMSnHvtn6ubUvaCf9iF8AIY= +github.com/ipfs/boxo v0.41.0/go.mod h1:1Fo36UVVvq3XAZwMDD82Cm4JTUi5x1k3AsJlg9DttOY= +github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= +github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= +github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= +github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= +github.com/ipfs/go-cid v0.6.1 h1:T5TnNb08+ueovG76Z5gx1L4Y7QOaGTXHg1F6raWFxIc= +github.com/ipfs/go-cid v0.6.1/go.mod h1:zrY0SwOhjrrIdfPQ/kf+k1sXyJ0QE7cMxfCployLBs0= +github.com/ipfs/go-cidutil v0.1.1 h1:COuby6H8C2ml0alvHYX3WdbFM4F07YtbY0UlT5j+sgI= +github.com/ipfs/go-cidutil v0.1.1/go.mod h1:SCoUftGEUgoXe5Hjeyw5CiLZF8cwYn/TbtpFQXJCP6k= +github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= +github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ds-badger4 v0.1.8 h1:frNczf5CjCVm62RJ5mW5tD/oLQY/9IKAUpKviRV9QAI= +github.com/ipfs/go-ds-badger4 v0.1.8/go.mod h1:FdqSLA5TMsyqooENB/Hf4xzYE/iH0z/ErLD6ogtfMrA= +github.com/ipfs/go-ds-dynamodb v0.3.0 h1:Zh947W14wvZYEDF5g42PJPEvHWKxD8k+TqCp7tO84Fk= +github.com/ipfs/go-ds-dynamodb v0.3.0/go.mod h1:Y+0KOICCda/1Nme0eV2T24pPvcNhhKYGgk67PPMAvJ8= +github.com/ipfs/go-ds-leveldb v0.5.2 h1:6nmxlQ2zbp4LCNdJVsmHfs9GP0eylfBNxpmY1csp0x0= +github.com/ipfs/go-ds-leveldb v0.5.2/go.mod h1:2fAwmcvD3WoRT72PzEekHBkQmBDhc39DJGoREiuGmYo= +github.com/ipfs/go-dsqueue v0.2.0 h1:MBi9w3oSiX98Xc+Y7NuJ9G8MI6mAT4IGdO9dHEMCZzU= +github.com/ipfs/go-dsqueue v0.2.0/go.mod h1:8FfNQC4DMF/KkzBXRNB9Rb3MKDW0Sh98HMtXYl1mLQE= +github.com/ipfs/go-ipfs-cmds v0.16.1 h1:O3xV6v2LN52wL0odvXX6jqlt7G2scuHzQYl80OJ+TOA= +github.com/ipfs/go-ipfs-cmds v0.16.1/go.mod h1:UkHLmJ2MlbLPuUJ0wmuF1R91+DGnwKvcCoEW3MR5CNg= +github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= +github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-ipfs-pq v0.0.4 h1:U7jjENWJd1jhcrR8X/xHTaph14PTAK9O+yaLJbjqgOw= +github.com/ipfs/go-ipfs-pq v0.0.4/go.mod h1:9UdLOIIb99IFrgT0Fc53pvbvlJBhpUb4GJuAQf3+O2A= +github.com/ipfs/go-ipfs-redirects-file v0.1.2 h1:QCK7VtL91FH17KROVVy5KrzDx2hu68QvB2FTWk08ZQk= +github.com/ipfs/go-ipfs-redirects-file v0.1.2/go.mod h1:yIiTlLcDEM/8lS6T3FlCEXZktPPqSOyuY6dEzVqw7Fw= +github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= +github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= +github.com/ipfs/go-ipld-format v0.6.3 h1:9/lurLDTotJpZSuL++gh3sTdmcFhVkCwsgx2+rAh4j8= +github.com/ipfs/go-ipld-format v0.6.3/go.mod h1:74ilVN12NXVMIV+SrBAyC05UJRk0jVvGqdmrcYZvCBk= +github.com/ipfs/go-ipld-legacy v0.3.0 h1:7XhFKkRyCvP5upOlQfKUFIqL3S5DEZnbUE4bQmQ/tNE= +github.com/ipfs/go-ipld-legacy v0.3.0/go.mod h1:Ukef9ARQiX+RVetwH2XiReLgJvQDEXcUPszrZ1KRjKI= +github.com/ipfs/go-log/v2 v2.9.2 h1:O/5BB0elpkRILvT24rCJ5976wWd7u0nJ436T3rdYdc4= +github.com/ipfs/go-log/v2 v2.9.2/go.mod h1:RziRwwXWhndlk8L75RnEe0zeAYaq2heKtEMc3jqUov0= +github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= +github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= +github.com/ipfs/go-peertaskqueue v0.8.3 h1:tBPpGJy+A92RqtRFq5amJn0Uuj8Pw8tXi0X3eHfHM8w= +github.com/ipfs/go-peertaskqueue v0.8.3/go.mod h1:OqVync4kPOcXEGdj/LKvox9DCB5mkSBeXsPczCxLtYA= +github.com/ipfs/go-test v0.3.0 h1:0Y4Uve3tp9HI+2lIJjfOliOrOgv/YpXg/l1y3P4DEYE= +github.com/ipfs/go-test v0.3.0/go.mod h1:JK+U8pRpATZb7lsYNSJlCj3WYB3cFfWIbI6nWRM/GFk= +github.com/ipfs/go-unixfsnode v1.10.4 h1:cMmMyOrSjQkPVQbQvt8trErIn6jhayNf9pBA9oOwfxY= +github.com/ipfs/go-unixfsnode v1.10.4/go.mod h1:Vu1e/s7ToALBBRo38sJ8DwUVWmSeQMTdxk5/rcHl7d0= +github.com/ipld/go-car/v2 v2.17.0 h1:zgjSxf/lQNYcQPX08cvb5rSdEY8sv5OOnQIsZhZMPx4= +github.com/ipld/go-car/v2 v2.17.0/go.mod h1:/4HY8tFZ1q42Mw54ILLPQfjkUqMJxFKqY1yMDKHlYko= +github.com/ipld/go-codec-dagpb v1.7.0 h1:hpuvQjCSVSLnTnHXn+QAMR0mLmb1gA6wl10LExo2Ts0= +github.com/ipld/go-codec-dagpb v1.7.0/go.mod h1:rD3Zg+zub9ZnxcLwfol/OTQRVjaLzXypgy4UqHQvilM= +github.com/ipld/go-ipld-prime v0.24.0 h1:6th8Z6Peh5bCWuRAVZcDO1sHzZdVF6F2cCCDG3681tg= +github.com/ipld/go-ipld-prime v0.24.0/go.mod h1:DYZxr/5caLNFbcuU6zLOgwSW7CgUEoC4wJiZMEU8Zhs= +github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20250821084354-a425e60cd714 h1:cqNk8PEwHnK0vqWln+U/YZhQc9h2NB3KjUjDPZo5Q2s= +github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20250821084354-a425e60cd714/go.mod h1:ZEUdra3CoqRVRYgAX/jAJO9aZGz6SKtKEG628fHHktY= +github.com/ipshipyard/p2p-forge v0.9.0 h1:Mp/bZ8BX7sxNTyzN5BXbYpOPbggrUbn+Dr5XnJ2kj0s= +github.com/ipshipyard/p2p-forge v0.9.0/go.mod h1:1keK1MRRCu5oNe9uFKfNIIZXOFEF9hgD1iK1DUsjsXQ= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.1 h1:oKHx3lgN4e5Nno2LKTMrVx+b+NkDptkO9aDireiBDGE= +github.com/letsencrypt/pebble/v2 v2.10.1/go.mod h1:KtYhQ4YTjT5MtoCZ6RTCXlbrrz6cKyXROCuTpIUDJFY= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+skhePFdE= +github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 h1:Y1LIBzKrtU/NbptugPNuem74f355oyWdLRIm0edgozo= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.40.0 h1:as8U7Y1RX9CTKCBiFBHWKZ6tSS+rE+6WNz+H1+M+wbo= +github.com/libp2p/go-libp2p-kad-dht v0.40.0/go.mod h1:iLUjII47u3/HjxyhucI2lhsl29lrzlAs/ym16+H40jE= +github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= +github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= +github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= +github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 h1:0lgqHvJWHLGW5TuObJrfyEi6+ASTKDBWikGvPqy9Yiw= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5Ow/QHMw8Nh3gPCo= +github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= +github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.90.0 h1:58BfEsP+G4uIRD9ApJTFsag+Mw+QQlZuH9uI/lPmjfY= +github.com/polydawn/refmt v0.90.0/go.mod h1:XAlDMOunevTYDsZtOKQd8itHXFMsX/QtDkPHaj6ZLxk= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/exporter-toolkit v0.16.0 h1:xT/j7L2XKF+VJd6B4fpUw6xWabHrSmsUf6mYmFqyu0s= +github.com/prometheus/exporter-toolkit v0.16.0/go.mod h1:d1EL8Z9674xQe/iWhwP2wDyCEoBPbXVeqDbqAUsgJWY= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/slok/go-http-metrics v0.13.0 h1:lQDyJJx9wKhmbliyUsZ2l6peGnXRHjsjoqPt5VYzcP8= +github.com/slok/go-http-metrics v0.13.0/go.mod h1:HIr7t/HbN2sJaunvnt9wKP9xoBBVZFo1/KiHU3b0w+4= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ= +github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM= +github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= +github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= +github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc h1:BCPnHtcboadS0DvysUuJXZ4lWVv5Bh5i7+tbIyi+ck4= +github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc/go.mod h1:r45hJU7yEoA81k6MWNhpMj/kms0n14dkzkxYHoB96UM= +github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= +github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= +github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= +golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE= +golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/test/autotls/harness.go b/test/autotls/harness.go new file mode 100644 index 00000000000..d4831951f86 --- /dev/null +++ b/test/autotls/harness.go @@ -0,0 +1,278 @@ +package autotls + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "net" + "net/http" + "os" + "testing" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + + pebbleCA "github.com/letsencrypt/pebble/v2/ca" + pebbleDB "github.com/letsencrypt/pebble/v2/db" + pebbleVA "github.com/letsencrypt/pebble/v2/va" + pebbleWFE "github.com/letsencrypt/pebble/v2/wfe" + + p2pforge "github.com/ipshipyard/p2p-forge/client" + + // Load CoreDNS + p2p-forge plugins (ipparser, acme). + _ "github.com/ipshipyard/p2p-forge/plugins" +) + +// Stack is an in-process pairing of Pebble (ACME server) and p2p-forge +// (registration HTTP endpoint + DNS server). It exists so the AutoTLS E2E +// canary can drive a real kubo daemon through the full cert-issuance flow +// without touching the public internet. +// +// Lifetimes are owned by the test: call Close (registered via t.Cleanup) +// when done. +type Stack struct { + // ForgeRegistrationEndpoint goes into AutoTLS.RegistrationEndpoint. + // The URL carries the production-shaped hostname (so the PeerID-auth + // signature scope matches what the forge server checks) plus the + // ?dial=/?dns= overrides that point the daemon at our loopback + // listeners. See docs/config.md for the full syntax. + ForgeRegistrationEndpoint string + + // ACMEEndpoint is Pebble's directory URL, for AutoTLS.CAEndpoint. + ACMEEndpoint string + + // PebbleCAPEM is the trust anchor for Pebble's HTTPS listener, for + // AutoTLS.TrustedCARootsPEM. Different from the issuance root below; + // kubo uses this only to authenticate the ACME directory connection. + PebbleCAPEM string + + // PebbleIssuanceRootPEM is the trust anchor for the certs Pebble + // issues. The canary's HTTPS client uses it to verify the AutoTLS + // cert kubo serves on /tls/http. + PebbleIssuanceRootPEM string + + // ForgeDomain is the suffix p2p-forge issues certs under. The canary + // uses a private .test suffix to avoid colliding with public DNS. + ForgeDomain string + + // ForgeAuthToken is the value of the registration endpoint's bearer + // token, also published to the forge process via p2pforge.ForgeAuthEnv. + ForgeAuthToken string + + close func() // shuts down both servers; idempotent via Stack.Close. +} + +// Close shuts down both servers. Safe to call multiple times. +func (s *Stack) Close() { + if s.close != nil { + s.close() + s.close = nil + } +} + +// NewStack stands up Pebble and p2p-forge on free local ports. Returns a +// Stack populated with the endpoint URLs and trust material the kubo +// daemon needs to talk to them. The setup mirrors the canonical pattern +// used in p2p-forge's own end-to-end test (e2e_test.go in that repo) so a +// regression in either layer surfaces here. +func NewStack(t *testing.T) *Stack { + t.Helper() + + const ( + forgeDomain = "libp2p.test" + forgeRegHost = "registration.libp2p.test" + forgeAuthToken = "test-token" + ) + + // p2p-forge reads its auth token from this env var. Pebble skips its + // random 0-15s VA sleep when PEBBLE_VA_NOSLEEP is set; without that + // the canary spends most of its budget idle. We restore both on + // cleanup so other parallel tests don't observe a leak. + prevAuth, hadAuth := setEnv(t, p2pforge.ForgeAuthEnv, forgeAuthToken) + prevNoSleep, hadNoSleep := setEnv(t, "PEBBLE_VA_NOSLEEP", "1") + + // Reserve a port for the forge HTTP registration endpoint. The + // acme plugin will bind it a moment later; reserving up-front lets + // the Corefile reference a known port. + tmpListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve forge http port: %v", err) + } + httpPort := tmpListener.Addr().(*net.TCPAddr).Port + _ = tmpListener.Close() + + // Configure the CoreDNS instance p2p-forge runs on. The ipparser + // plugin resolves . from the embedded IP; the + // acme plugin owns the DNS-01 TXT records and the HTTP registration + // endpoint. `.:0` lets CoreDNS pick free UDP and TCP ports; the + // running instance reports both via Servers()[0] below. + tmpDir := t.TempDir() + dnsserver.Directives = []string{"log", "whoami", "startup", "shutdown", "ipparser", "acme"} + corefile := fmt.Sprintf(`.:0 { + log + ipparser %[1]s + acme %[1]s { + registration-domain %[2]s listen-address=:%[3]d external-tls=true + database-type badger %[4]s + } + }`, forgeDomain, forgeRegHost, httpPort, tmpDir) + dnsInstance, err := caddy.Start(&corefileInput{body: []byte(corefile)}) + if err != nil { + t.Fatalf("start p2p-forge (caddy): %v", err) + } + // CoreDNS's default DNS server type exposes the UDP packet conn via + // LocalAddr() and the TCP listener via Addr(). Pebble v2.10 forces + // TCP for ACME DNS lookups, so its VA must dial the TCP listener; + // the kubo daemon's pre-flight check uses Go's net.Resolver with + // PreferGo, which sends TXT queries over UDP. + dnsUDPAddr, dnsTCPAddr := dnsServerAddresses(t, dnsInstance.Servers()[0]) + + // Stand up Pebble. The VA needs the forge DNS server's address so it + // can resolve the DNS-01 TXT records p2p-forge publishes. + pebbleLogger := log.New(os.Stderr, "pebble: ", log.LstdFlags) + db := pebbleDB.NewMemoryStore() + // ocspResponderURL="" (none), keyAlg="rsa" (matches p2p-forge's + // own e2e setup; pebble's GetRootKey only handles RSA so an ECDSA + // CA would panic if pebble's internal flows ever call it), 1 + // alternate root, chain length 1 (Pebble requires at least one + // intermediate). One issuance profile so wfe.NewOrder's + // random-profile pick doesn't Intn(0)-panic. + profiles := map[string]pebbleCA.Profile{ + "shortlived": { + Description: "Pebble test profile", + ValidityPeriod: 7 * 24 * 60 * 60, // seconds + }, + } + ca := pebbleCA.New(pebbleLogger, db, "", "rsa", 1, 1, profiles) + va := pebbleVA.New(pebbleLogger, 0, 0, false, dnsTCPAddr, db) + // nil caaIdentities skips CAA checks (we have no DNS CAA records for + // libp2p.test anyway). strict=false, requireEAB=false, retryAfter + // values match Pebble's defaults. + wfe := pebbleWFE.New(pebbleLogger, db, va, ca, nil, false, false, 3, 5) + + // Pebble's ACME endpoint must speak HTTPS. The self-signed cert + // below is what AutoTLS.TrustedCARootsPEM teaches kubo's certmagic + // to trust. + pebbleCertPEM, pebbleKeyPEM, err := generateLoopbackCert("127.0.0.1") + if err != nil { + t.Fatalf("pebble self-signed cert: %v", err) + } + pebbleCert, err := tls.X509KeyPair(pebbleCertPEM, pebbleKeyPEM) + if err != nil { + t.Fatalf("load pebble cert: %v", err) + } + acmeListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen pebble: %v", err) + } + tlsListener := tls.NewListener(acmeListener, &tls.Config{Certificates: []tls.Certificate{pebbleCert}}) + acmeServer := &http.Server{Handler: wfe.Handler()} + go func() { _ = acmeServer.Serve(tlsListener) }() + + stack := &Stack{ + ForgeRegistrationEndpoint: fmt.Sprintf("http://%s/?dial=127.0.0.1:%d&dns=%s", forgeRegHost, httpPort, dnsUDPAddr), + ACMEEndpoint: fmt.Sprintf("https://%s%s", acmeListener.Addr(), pebbleWFE.DirectoryPath), + PebbleCAPEM: string(pebbleCertPEM), + PebbleIssuanceRootPEM: string(ca.GetRootCert(0).PEM()), + ForgeDomain: forgeDomain, + ForgeAuthToken: forgeAuthToken, + close: func() { + _ = acmeServer.Close() + _ = dnsInstance.Stop() + dnsInstance.Wait() + restoreEnv(p2pforge.ForgeAuthEnv, prevAuth, hadAuth) + restoreEnv("PEBBLE_VA_NOSLEEP", prevNoSleep, hadNoSleep) + }, + } + t.Cleanup(stack.Close) + return stack +} + +// dnsServerAddresses returns the UDP and TCP listener addresses from a +// CoreDNS ServerListener. The default DNS server type binds the UDP packet +// conn to LocalAddr() and the TCP listener to Addr(); a swap to +// DoH/DoT/DoQ/gRPC would fail one of the assertions loudly. Mirrors the +// helper in p2p-forge's own e2e test. +func dnsServerAddresses(t *testing.T, srv caddy.ServerListener) (udpAddr, tcpAddr string) { + t.Helper() + pkt := srv.LocalAddr() + if pkt == nil || pkt.Network() != "udp" { + t.Fatalf("expected UDP packet conn on CoreDNS server, got %v", pkt) + } + l := srv.Addr() + if l == nil || l.Network() != "tcp" { + t.Fatalf("expected TCP listener on CoreDNS server, got %v", l) + } + return pkt.String(), l.String() +} + +// setEnv sets key=val for the duration of the test, returning the previous +// value (if any) so a paired restoreEnv call can put it back on cleanup. We +// roll our own instead of t.Setenv because the canary uses t.Parallel. +func setEnv(t *testing.T, key, val string) (prev string, had bool) { + t.Helper() + prev, had = os.LookupEnv(key) + if err := os.Setenv(key, val); err != nil { + t.Fatalf("set %s: %v", key, err) + } + return prev, had +} + +// restoreEnv puts an environment variable back to the value setEnv saw. +func restoreEnv(key, prev string, had bool) { + if had { + _ = os.Setenv(key, prev) + } else { + _ = os.Unsetenv(key) + } +} + +// generateLoopbackCert produces a self-signed cert + private key covering +// the supplied IP. Used for Pebble's HTTPS listener; kubo's certmagic +// trusts this cert via AutoTLS.TrustedCARootsPEM. +func generateLoopbackCert(ipAddr string) ([]byte, []byte, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, err + } + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{Organization: []string{"kubo autotls e2e"}}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: []net.IP{net.ParseIP(ipAddr)}, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + keyDER, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, nil, err + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + return certPEM, keyPEM, nil +} + +// corefileInput implements the caddy.Input interface around a raw Corefile. +type corefileInput struct{ body []byte } + +func (i *corefileInput) Body() []byte { return i.body } +func (i *corefileInput) Path() string { return "Corefile" } +func (i *corefileInput) ServerType() string { return "dns" } diff --git a/test/autotls/zones/.gitignore b/test/autotls/zones/.gitignore new file mode 100644 index 00000000000..eaf6a47ac57 --- /dev/null +++ b/test/autotls/zones/.gitignore @@ -0,0 +1,5 @@ +# The forge canary's DNS zone file is literally named after the forge +# domain (libp2p.test) so the p2p-forge ipparser plugin can find it. +# Override the root *.test rule (intended for Go test binaries) for this +# one directory. +!libp2p.test diff --git a/test/autotls/zones/libp2p.test b/test/autotls/zones/libp2p.test new file mode 100644 index 00000000000..0d4c9c27967 --- /dev/null +++ b/test/autotls/zones/libp2p.test @@ -0,0 +1,18 @@ +; Minimal DNS zone for the AutoTLS E2E canary. +; The p2p-forge ipparser plugin reads this at CoreDNS startup; it needs +; an SOA + NS record for the forge domain. We don't need glue records +; or A/AAAA entries: ipparser synthesises per-peer subdomains from the +; embedded IP in .libp2p.test hostnames at query time. +$TTL 300 +$ORIGIN libp2p.test. + +@ 86400 IN SOA ns1.libp2p.test. admin.libp2p.test. ( + 2026010101 ; serial + 86400 ; refresh + 2400 ; retry + 604800 ; expire + 86400 ; minimum +) + +libp2p.test. 86400 IN NS ns1.libp2p.test. +ns1.libp2p.test. 86400 IN A 127.0.0.1 diff --git a/test/cli/content_blocking_test.go b/test/cli/content_blocking_test.go index 27618d7d616..32966768ce0 100644 --- a/test/cli/content_blocking_test.go +++ b/test/cli/content_blocking_test.go @@ -71,11 +71,12 @@ func TestContentBlocking(t *testing.T) { os.Setenv("IPFS_NS_MAP", "blocked-cid.example.com:/ipfs/"+blockedCID+",blocked-dnslink.example.com/ipns/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") defer os.Unsetenv("IPFS_NS_MAP") - // Enable GatewayOverLibp2p as we want to test denylist there too - node.IPFS("config", "--json", "Experimental.GatewayOverLibp2p", "true") + // Enable HTTPProvider.Libp2p so the denylist is exercised on that transport too. + node.IPFS("config", "--json", "HTTPProvider.Enabled", "true") + node.IPFS("config", "--json", "HTTPProvider.Libp2p", "true") // Start daemon, it should pick up denylist from $IPFS_PATH/denylists/test.deny - node.StartDaemon() // we need online mode for GatewayOverLibp2p tests + node.StartDaemon() // online mode is needed for HTTPProvider.Libp2p tests t.Cleanup(func() { node.StopDaemon() }) client := node.GatewayClient() @@ -304,14 +305,13 @@ func TestContentBlocking(t *testing.T) { client = node.GatewayClient() }) - // We need to confirm denylist is active on the - // trustless gateway exposed over libp2p - // when Experimental.GatewayOverLibp2p=true - // (https://github.com/ipfs/kubo/blob/master/docs/experimental-features.md#http-gateway-over-libp2p) - // NOTE: this type of gateway is hardcoded to be NoFetch: it does not fetch - // data that is not in local store, so we only need to run it once: a - // simple smoke-test for allowed CID and blockedCID. - t.Run("GatewayOverLibp2p", func(t *testing.T) { + // Confirm the denylist is active on the trustless gateway exposed + // over libp2p when HTTPProvider.Libp2p=true (see + // https://github.com/ipfs/kubo/blob/master/docs/config.md#httpproviderlibp2p). + // This transport is hardcoded to NoFetch: it does not fetch data that + // is not in the local blockstore, so a single smoke test of an + // allowed CID and the blockedCID is enough. + t.Run("HTTPProviderLibp2p", func(t *testing.T) { t.Parallel() // Create libp2p client that connects to our node over diff --git a/test/cli/daemon_test.go b/test/cli/daemon_test.go index f87a2165148..6f3090b7f7c 100644 --- a/test/cli/daemon_test.go +++ b/test/cli/daemon_test.go @@ -41,9 +41,10 @@ func TestDaemon(t *testing.T) { // Enable experimental features and pubsub via config node.UpdateConfig(func(cfg *config.Config) { - cfg.Pubsub.Enabled = config.True // Instead of --enable-pubsub-experiment - cfg.Experimental.P2pHttpProxy = true // Enable P2P HTTP proxy - cfg.Experimental.GatewayOverLibp2p = true // Enable gateway over libp2p + cfg.Pubsub.Enabled = config.True // Instead of --enable-pubsub-experiment + cfg.Experimental.P2pHttpProxy = true // Enable P2P HTTP proxy + cfg.HTTPProvider.Enabled = config.True + cfg.HTTPProvider.Libp2p = config.True // Serve gateway over libp2p stream }) node.StartDaemon("--enable-gc") diff --git a/test/cli/http_provider_autotls_test.go b/test/cli/http_provider_autotls_test.go new file mode 100644 index 00000000000..600649419a5 --- /dev/null +++ b/test/cli/http_provider_autotls_test.go @@ -0,0 +1,220 @@ +package cli + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "testing" + "time" + + gws "github.com/gorilla/websocket" + "github.com/ipfs/go-cid" + "github.com/ipfs/kubo/config" + "github.com/ipfs/kubo/test/cli/harness" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + "golang.org/x/net/http2" +) + +// TestHTTPProviderAutoTLS exercises the AutoTLS HTTPS path of HTTPProvider +// using a self-signed test cert (AutoTLS.SelfSignedForTests=true). Test +// clients pair this with InsecureSkipVerify to skip cert chain validation. +// +// The full p2p-forge + ACME chain is covered separately by the canary in +// http_provider_autotls_e2e_test.go; the cases here focus on listener +// wiring, the kubo-side h2-over-TLS policy, and the multiaddr/well-known +// surfaces that browsers and HTTP retrieval clients depend on. +func TestHTTPProviderAutoTLS(t *testing.T) { + t.Parallel() + nodes := harness.NewT(t).NewNodes(2).Init() + gwNode := nodes[0] + otherNode := nodes[1] + + // Add a /tls/ws listener. AutoWSS normally adds /tls/sni//ws + // for AutoTLS; the test bypasses AutoTLS in favour of the + // SelfSignedForTests path, so a plain /tls/ws is enough. The client + // uses InsecureSkipVerify, so SNI is decorative. + gwNode.UpdateConfig(func(cfg *config.Config) { + cfg.Addresses.Swarm = append(cfg.Addresses.Swarm, + "/ip4/127.0.0.1/tcp/0/tls/ws") + cfg.AutoTLS.Enabled = config.False + cfg.AutoTLS.SelfSignedForTests = config.True + cfg.HTTPProvider.Enabled = config.True + // AnnouncesTLSHTTPMultiaddr below needs the /tls/http announcement. + cfg.HTTPProvider.AnnounceMultiaddrs = config.True + }) + + nodes.StartDaemons().Connect() + defer nodes.StopDaemons() + t.Cleanup(func() { gwNode.StopDaemon() }) + + cidLocal := cid.MustParse(gwNode.IPFSAddStr("Hello AutoTLS HTTPProvider!")) + expectedRawBlock := []byte(gwNode.GatewayClient().Get( + fmt.Sprintf("/ipfs/%s?format=raw", cidLocal), + ).Body) + cidRemote := cid.MustParse(otherNode.IPFSAddStr("not on the gateway")) + + hostPort := findTLSWSListenHostPort(t, gwNode) + // SNI must match the cert's wildcard SAN; the listener was configured + // to match the example.libp2p.direct subdomain. + const tlsServerName = "example.libp2p.direct" + baseURL := "https://" + tlsServerName + ":" + portOf(t, hostPort) + dialAddr := hostPort // raw IP:port we actually dial + + t.Run("ServeBlock_HTTPSh2", func(t *testing.T) { + // h2 over TLS: the gateway handler returns the raw block. + resp := mustGetHTTPS(t, baseURL+"/ipfs/"+cidLocal.String()+"?format=raw", + []string{"h2", "http/1.1"}, dialAddr, tlsServerName) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "HTTP/2.0", resp.Proto) + require.Equal(t, "application/vnd.ipld.raw", resp.Header.Get("Content-Type")) + require.Equal(t, expectedRawBlock, mustReadBody(t, resp)) + }) + + t.Run("HTTP1OverTLSRejected", func(t *testing.T) { + // kubo's RequireHTTP2OverTLS policy: HTTP/1.1 over TLS is + // reserved for the WebSocket upgrade. Plain h1 GET gets 426 + // with an Upgrade header pointing the client at h2 or + // websocket. + resp := mustGetHTTPS(t, baseURL+"/ipfs/"+cidLocal.String()+"?format=raw", + []string{"http/1.1"}, dialAddr, tlsServerName) + require.Equal(t, http.StatusUpgradeRequired, resp.StatusCode) + require.Contains(t, resp.Header.Values("Upgrade"), "h2,websocket") + }) + + t.Run("WSSUpgradeStillWorks", func(t *testing.T) { + // Browser-equivalent path: ALPN http/1.1 only, classic + // WebSocket Upgrade. Must complete with 101 Switching + // Protocols even when the listener also serves HTTP/2 GETs. + dialer := gws.Dialer{ + NetDial: func(network, _ string) (net.Conn, error) { + return net.Dial(network, dialAddr) + }, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: tlsServerName, + NextProtos: []string{"http/1.1"}, + }, + HandshakeTimeout: 5 * time.Second, + } + conn, resp, err := dialer.Dial("wss://"+tlsServerName+":"+portOf(t, hostPort)+"/", nil) + require.NoError(t, err) + require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode) + require.NoError(t, conn.Close()) + }) + + t.Run("WillNotServeRemoteContent", func(t *testing.T) { + resp := mustGetHTTPS(t, baseURL+"/ipfs/"+cidRemote.String()+"?format=raw", + []string{"h2"}, dialAddr, tlsServerName) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("ServesWellKnownProtocolsOverHTTPS", func(t *testing.T) { + resp := mustGetHTTPS(t, baseURL+"/.well-known/libp2p/protocols", + []string{"h2"}, dialAddr, tlsServerName) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "application/json", resp.Header.Get("Content-Type")) + var doc map[string]map[string]string + require.NoError(t, json.Unmarshal(mustReadBody(t, resp), &doc)) + require.Contains(t, doc, "/ipfs/gateway") + }) + + t.Run("AnnouncesTLSHTTPMultiaddr", func(t *testing.T) { + announced := announcedAddrs(t, gwNode) + var tlsWS, tlsHTTP []multiaddr.Multiaddr + for _, a := range announced { + s := a.String() + switch { + case strings.HasSuffix(s, "/tls/ws") || + strings.Contains(s, "/tls/sni/") && strings.HasSuffix(s, "/ws"): + tlsWS = append(tlsWS, a) + case strings.HasSuffix(s, "/tls/http") || + strings.Contains(s, "/tls/sni/") && strings.HasSuffix(s, "/http"): + tlsHTTP = append(tlsHTTP, a) + } + } + require.NotEmpty(t, tlsWS, "expected at least one /tls/ws in announced addrs") + require.Equal(t, len(tlsWS), len(tlsHTTP), + "every announced /tls/ws should have a sibling /tls/http (ws=%v http=%v)", + addrStrings(tlsWS), addrStrings(tlsHTTP)) + }) +} + +// findTLSWSListenHostPort returns "host:port" for a /tls/ws listener (in any +// of its forms: /tls/ws, /tls/sni//ws). The TCP port is what we dial; +// the SNI is set separately on the TLS client config. +func findTLSWSListenHostPort(t *testing.T, n *harness.Node) string { + t.Helper() + for _, a := range n.SwarmAddrs() { + s := a.String() + if !strings.HasSuffix(s, "/ws") || !strings.Contains(s, "/tls/") { + continue + } + host, err := a.ValueForProtocol(multiaddr.P_IP4) + require.NoError(t, err) + port, err := a.ValueForProtocol(multiaddr.P_TCP) + require.NoError(t, err) + return net.JoinHostPort(host, port) + } + t.Fatalf("no /tls/ws listener found on node %d; addrs=%v", n.ID, addrStrings(n.SwarmAddrs())) + return "" +} + +// portOf extracts the port from a host:port string. +func portOf(t *testing.T, hostPort string) string { + t.Helper() + _, port, err := net.SplitHostPort(hostPort) + require.NoError(t, err) + return port +} + +// mustGetHTTPS issues a TLS GET, dialing dialAddr (raw IP:port) but +// presenting tlsServerName as SNI and Host. ALPN is restricted to +// nextProtos so tests can pin which HTTP version they want negotiated. +// InsecureSkipVerify is on because the daemon serves a self-signed cert. +func mustGetHTTPS(t *testing.T, url string, nextProtos []string, dialAddr, tlsServerName string) *http.Response { + t.Helper() + tlsConf := &tls.Config{ + InsecureSkipVerify: true, + ServerName: tlsServerName, + NextProtos: nextProtos, + } + dialContext := func(network, _ string) (net.Conn, error) { + return net.Dial(network, dialAddr) + } + var rt http.RoundTripper + switch { + case len(nextProtos) > 0 && nextProtos[0] == "h2": + // HTTP/2 only. + rt = &http2.Transport{ + TLSClientConfig: tlsConf, + DialTLS: func(network, _ string, cfg *tls.Config) (net.Conn, error) { + raw, err := dialContext(network, "") + if err != nil { + return nil, err + } + tlsConn := tls.Client(raw, cfg) + if err := tlsConn.Handshake(); err != nil { + _ = raw.Close() + return nil, err + } + return tlsConn, nil + }, + } + default: + // HTTP/1.1 (or h2/h1 with ALPN selection). + rt = &http.Transport{ + ForceAttemptHTTP2: false, + TLSClientConfig: tlsConf, + Dial: dialContext, + } + } + c := &http.Client{Transport: rt, Timeout: 10 * time.Second} + resp, err := c.Get(url) + require.NoError(t, err) + t.Cleanup(func() { _ = resp.Body.Close() }) + return resp +} diff --git a/test/cli/http_gateway_over_libp2p_test.go b/test/cli/http_provider_over_libp2p_test.go similarity index 86% rename from test/cli/http_gateway_over_libp2p_test.go rename to test/cli/http_provider_over_libp2p_test.go index 58ab0217ba0..c6f11fc3ec6 100644 --- a/test/cli/http_gateway_over_libp2p_test.go +++ b/test/cli/http_provider_over_libp2p_test.go @@ -19,7 +19,12 @@ import ( "github.com/stretchr/testify/require" ) -func TestGatewayOverLibp2p(t *testing.T) { +// TestHTTPProviderOverLibp2p exercises the libp2p-stream transport of the +// HTTPProvider feature: the trustless gateway handler reachable via the +// /ipfs/gateway protocol over a libp2p stream. The peer hosting the data +// also mounts an HTTP-to-libp2p proxy listener so requests can be made by +// stock HTTP clients. +func TestHTTPProviderOverLibp2p(t *testing.T) { t.Parallel() nodes := harness.NewT(t).NewNodes(2).Init() @@ -58,13 +63,14 @@ func TestGatewayOverLibp2p(t *testing.T) { p2pProxyNodeHTTPListenAddr, err := manet.ToNetAddr(p2pProxyNodeHTTPListenMA) require.NoError(t, err) - t.Run("DoesNotWorkWithoutExperimentalConfig", func(t *testing.T) { + t.Run("DoesNotWorkWithoutHTTPProvider", func(t *testing.T) { _, err := http.Get(fmt.Sprintf("http://%s/ipfs/%s?format=raw", p2pProxyNodeHTTPListenAddr, cidDataOnGatewayNode)) require.Error(t, err) }) - // Enable the experimental feature and reconnect the nodes - gwNode.IPFS("config", "--json", "Experimental.GatewayOverLibp2p", "true") + // Enable HTTPProvider and the libp2p-stream transport, then reconnect. + gwNode.IPFS("config", "--json", "HTTPProvider.Enabled", "true") + gwNode.IPFS("config", "--json", "HTTPProvider.Libp2p", "true") gwNode.StopDaemon().StartDaemon() t.Cleanup(func() { gwNode.StopDaemon() }) nodes.Connect() diff --git a/test/cli/http_provider_test.go b/test/cli/http_provider_test.go new file mode 100644 index 00000000000..ef67ec567fa --- /dev/null +++ b/test/cli/http_provider_test.go @@ -0,0 +1,283 @@ +package cli + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "testing" + "time" + + gws "github.com/gorilla/websocket" + "github.com/ipfs/go-cid" + "github.com/ipfs/kubo/config" + "github.com/ipfs/kubo/test/cli/harness" + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/peer" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + "golang.org/x/net/http2" +) + +// TestHTTPProvider exercises the cleartext HTTP transport of the HTTPProvider +// feature: the trustless gateway handler reachable over plain /ws and /http +// on the same TCP port shared with libp2p WebSocket traffic. The TLS path is +// covered separately in http_provider_autotls_test.go. +func TestHTTPProvider(t *testing.T) { + t.Parallel() + nodes := harness.NewT(t).NewNodes(2).Init() + gwNode := nodes[0] + otherNode := nodes[1] + + // Add a cleartext /ws listener on the gateway node so the HTTPProvider + // handler has somewhere to live without needing AutoTLS in this test. + // Sharing port 0 with the existing /tcp/0 means tcpreuse routes raw + // libp2p, plain HTTP, and h2c into the same socket โ€” the production + // layout, just without TLS. + gwNode.UpdateConfig(func(cfg *config.Config) { + cfg.Addresses.Swarm = append(cfg.Addresses.Swarm, "/ip4/127.0.0.1/tcp/0/ws") + }) + + nodes.StartDaemons().Connect() + defer nodes.StopDaemons() + + // Add data on the gateway node and capture the local raw block so + // later byte-by-byte comparisons are exact. + cidLocal := cid.MustParse(gwNode.IPFSAddStr("Hello HTTPProvider!")) + expectedRawBlock := []byte(gwNode.GatewayClient().Get( + fmt.Sprintf("/ipfs/%s?format=raw", cidLocal), + ).Body) + + // And on the other node, so we have a CID the gateway does NOT have + // locally for the NoFetch test. + cidRemote := cid.MustParse(otherNode.IPFSAddStr("not on the gateway")) + + wsHostPort := findWSListenHostPort(t, gwNode) + baseURL := "http://" + wsHostPort + + t.Run("DoesNotWorkWithoutHTTPProvider", func(t *testing.T) { + // HTTPProvider is off by default; the listener answers 404 for + // non-WebSocket requests because no fallback handler is wired. + resp := mustGetH1(t, baseURL+"/ipfs/"+cidLocal.String()+"?format=raw") + require.Equal(t, http.StatusNotFound, resp.StatusCode, + "non-upgrade HTTP requests must 404 until HTTPProvider is enabled") + }) + + // AnnouncesHTTPMultiaddr below needs both Enabled and + // AnnounceMultiaddrs. Restart so FX picks up the handler. + gwNode.IPFS("config", "--json", "HTTPProvider.Enabled", "true") + gwNode.IPFS("config", "--json", "HTTPProvider.AnnounceMultiaddrs", "true") + gwNode.StopDaemon().StartDaemon() + t.Cleanup(func() { gwNode.StopDaemon() }) + nodes.Connect() + + // Refresh: the random port may differ across daemon restarts. + wsHostPort = findWSListenHostPort(t, gwNode) + baseURL = "http://" + wsHostPort + + t.Run("WillNotServeRemoteContent", func(t *testing.T) { + // NoFetch invariant: the gateway node does not have cidRemote + // locally and must not reach out to fetch it. + resp := mustGetH1(t, baseURL+"/ipfs/"+cidRemote.String()+"?format=raw") + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("WillNotServeDeserializedResponses", func(t *testing.T) { + // DeserializedResponses=false invariant: a request without + // ?format=raw|car asks for deserialized UnixFS, which the + // trustless gateway refuses. + resp := mustGetH1(t, baseURL+"/ipfs/"+cidLocal.String()) + require.Equal(t, http.StatusNotAcceptable, resp.StatusCode) + }) + + t.Run("ServeBlock_HTTP1", func(t *testing.T) { + resp := mustGetH1(t, baseURL+"/ipfs/"+cidLocal.String()+"?format=raw") + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "HTTP/1.1", resp.Proto) + require.Equal(t, "application/vnd.ipld.raw", resp.Header.Get("Content-Type")) + require.Equal(t, expectedRawBlock, mustReadBody(t, resp)) + }) + + t.Run("ServeBlock_H2C", func(t *testing.T) { + // Reverse-proxy interop: h2c gives multiplexing without TLS. + resp := mustGetH2C(t, baseURL+"/ipfs/"+cidLocal.String()+"?format=raw") + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "HTTP/2.0", resp.Proto) + require.Equal(t, "application/vnd.ipld.raw", resp.Header.Get("Content-Type")) + require.Equal(t, expectedRawBlock, mustReadBody(t, resp)) + }) + + t.Run("ServesWellKnownProtocols", func(t *testing.T) { + // libp2p Gateway spec advertisement, reachable from any HTTP + // client without opening a libp2p stream. + resp := mustGetH1(t, baseURL+"/.well-known/libp2p/protocols") + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "application/json", resp.Header.Get("Content-Type")) + var doc map[string]map[string]string + require.NoError(t, json.Unmarshal(mustReadBody(t, resp), &doc)) + require.Contains(t, doc, "/ipfs/gateway") + require.Equal(t, "/", doc["/ipfs/gateway"]["path"]) + }) + + t.Run("WSUpgradeStillWorks", func(t *testing.T) { + // The fallback handler must not break the original libp2p WS + // upgrade. A 101 Switching Protocols is enough; we don't + // complete the libp2p multistream handshake here. + dialer := gws.Dialer{HandshakeTimeout: 5 * time.Second} + conn, resp, err := dialer.Dial("ws://"+wsHostPort+"/", nil) + require.NoError(t, err) + require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode) + require.NoError(t, conn.Close()) + }) + + t.Run("AnnouncesHTTPMultiaddr", func(t *testing.T) { + // /http must appear next to every announced /ws. The /http + // multiaddr is purely an announcement and has no socket of + // its own, so read it via `ipfs id` rather than SwarmAddrs. + announced := announcedAddrs(t, gwNode) + var wsAddrs, httpAddrs []multiaddr.Multiaddr + for _, a := range announced { + s := a.String() + switch { + case strings.HasSuffix(s, "/ws"): + wsAddrs = append(wsAddrs, a) + case strings.HasSuffix(s, "/http"): + httpAddrs = append(httpAddrs, a) + } + } + require.NotEmpty(t, wsAddrs, "expected at least one /ws in announced addrs") + require.Equal(t, len(wsAddrs), len(httpAddrs), + "every announced /ws should have a sibling /http (ws=%v http=%v)", + addrStrings(wsAddrs), addrStrings(httpAddrs)) + }) + + t.Run("ServesWellKnownOverLibp2pStream", func(t *testing.T) { + // Sanity check that the libp2p-stream gateway also lands the + // gateway handler over the discovered protocol path. Mirrors + // http_provider_over_libp2p_test.go's libp2p-client subtest + // but uses libp2phttp's NamespacedClient discovery. + clientHost, err := libp2p.New(libp2p.NoListenAddrs) + require.NoError(t, err) + t.Cleanup(func() { _ = clientHost.Close() }) + require.NoError(t, clientHost.Connect(context.Background(), peer.AddrInfo{ + ID: gwNode.PeerID(), + Addrs: gwNode.SwarmAddrs(), + })) + client, err := (&libp2phttp.Host{StreamHost: clientHost}). + NamespacedClient("/ipfs/gateway", peer.AddrInfo{ID: gwNode.PeerID()}) + require.NoError(t, err) + resp, err := client.Get(fmt.Sprintf("/ipfs/%s?format=raw", cidLocal)) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, expectedRawBlock, body) + }) +} + +// findWSListenHostPort returns "host:port" for any /tcp/N/ws listener bound +// by the node. Auto-appended /ws shares the TCP port with raw /tcp/N via +// tcpreuse, so any matching listener is a fine target. +func findWSListenHostPort(t *testing.T, n *harness.Node) string { + t.Helper() + for _, a := range n.SwarmAddrs() { + s := a.String() + // Match exactly /tcp//ws (cleartext); skip /tls/ws etc. + if !strings.HasSuffix(s, "/ws") || strings.Contains(s, "/tls/") || strings.Contains(s, "/wss") { + continue + } + host, err := a.ValueForProtocol(multiaddr.P_IP4) + require.NoError(t, err) + port, err := a.ValueForProtocol(multiaddr.P_TCP) + require.NoError(t, err) + return net.JoinHostPort(host, port) + } + t.Fatalf("no cleartext /ws listener found on node %d; addrs=%v", n.ID, addrStrings(n.SwarmAddrs())) + return "" +} + +// announcedAddrs returns the multiaddrs this node advertises via identify +// (output of `ipfs id`), which is what other peers see in the DHT and via +// peer-routing. Differs from SwarmAddrs() in that it includes addresses +// that are advertised but not bound (the HTTPProvider /http multiaddr is +// the canonical example). +func announcedAddrs(t *testing.T, n *harness.Node) []multiaddr.Multiaddr { + t.Helper() + out := n.IPFS("id", "--enc=json").Stdout.String() + var idDoc struct { + Addresses []string `json:"Addresses"` + } + require.NoError(t, json.Unmarshal([]byte(out), &idDoc)) + addrs := make([]multiaddr.Multiaddr, 0, len(idDoc.Addresses)) + for _, s := range idDoc.Addresses { + // Strip the trailing /p2p/ wrapper for easier suffix + // matching downstream. + if i := strings.Index(s, "/p2p/"); i >= 0 { + s = s[:i] + } + a, err := multiaddr.NewMultiaddr(s) + if err != nil { + t.Fatalf("parse announced addr %q: %s", s, err) + } + addrs = append(addrs, a) + } + return addrs +} + +// addrStrings is a small log-friendly view of a multiaddr slice. +func addrStrings(addrs []multiaddr.Multiaddr) []string { + out := make([]string, len(addrs)) + for i, a := range addrs { + out[i] = a.String() + } + return out +} + +// mustGetH1 performs a plain HTTP/1.1 GET. Caller must close resp.Body. +func mustGetH1(t *testing.T, url string) *http.Response { + t.Helper() + tr := &http.Transport{ + // Force HTTP/1.1: an empty NextProtos in TLS skips ALPN, but + // for cleartext h1 is the default. + } + defer tr.CloseIdleConnections() + c := &http.Client{Transport: tr, Timeout: 10 * time.Second} + resp, err := c.Get(url) + require.NoError(t, err) + t.Cleanup(func() { _ = resp.Body.Close() }) + return resp +} + +// mustGetH2C performs a prior-knowledge HTTP/2 cleartext (h2c) GET. The +// AllowHTTP+DialTLSContext combo is the canonical way to drive h2c from a +// Go client โ€” it tells http2.Transport to skip ALPN negotiation and assume +// the server speaks h2c on the wire. +func mustGetH2C(t *testing.T, url string) *http.Response { + t.Helper() + tr := &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(_ context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + } + defer tr.CloseIdleConnections() + c := &http.Client{Transport: tr, Timeout: 10 * time.Second} + resp, err := c.Get(url) + require.NoError(t, err) + t.Cleanup(func() { _ = resp.Body.Close() }) + return resp +} + +// mustReadBody drains and returns the response body. +func mustReadBody(t *testing.T, resp *http.Response) []byte { + t.Helper() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + return body +} diff --git a/test/cli/http_retrieval_verify_test.go b/test/cli/http_retrieval_verify_test.go new file mode 100644 index 00000000000..13865386f24 --- /dev/null +++ b/test/cli/http_retrieval_verify_test.go @@ -0,0 +1,262 @@ +package cli + +import ( + crand "crypto/rand" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/ipfs/boxo/routing/http/server" + "github.com/ipfs/boxo/routing/http/types" + "github.com/ipfs/go-cid" + "github.com/ipfs/kubo/config" + "github.com/ipfs/kubo/test/cli/harness" + "github.com/ipfs/kubo/test/cli/testutils/httprouting" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" +) + +// HTTP retrieval lets Kubo fetch blocks straight from HTTP gateways that +// advertise themselves through /routing/v1 (config: HTTPRetrieval.Enabled). +// Kubo relies on this path more over time, and #11333 adds the serving side: a +// Kubo node can serve blocks over native HTTPS. As the set of HTTP providers +// grows, more of them run on machines and software outside the project's +// control. +// +// An HTTP endpoint can return the wrong bytes for many ordinary reasons: +// a caching proxy in front of it, a misconfigured rewrite, a truncated +// response, a buggy or hostile operator. Kubo treats provider bytes as +// untrusted and verifies they hash to the requested CID before handing them to +// the user. That check is the content-addressing guarantee. +// +// These tests are belt-and-suspenders coverage that the guarantee holds end to +// end (the Kubo daemon, its bitswap HTTP client in boxo, and the /routing/v1 +// lookup), so a regression anywhere in that chain (in Kubo or an upstream +// library) trips here instead of reaching a user as silently corrupt data. +// They run on loopback with no internet dependency. +// +// Out of scope: trustless CAR responses (the bitswap HTTP client requests raw +// blocks only) and multi-block DAGs. A single raw block is the smallest case +// that exercises fetch-and-verify, so that is what these use. + +// httpRetrievalNoLeakTimeout bounds the "fails closed" assertion below. When the +// only provider serves corrupt bytes, bitswap keeps re-finding and re-trying it, +// so `ipfs cat` never returns on its own. We cap it and assert it failed without +// emitting the bad bytes. It only needs to be long enough for the daemon to +// connect to the provider and fetch (then reject) the block at least once, which +// happens in about a second on loopback; 10s leaves ample margin on slow CI. +const httpRetrievalNoLeakTimeout = 10 * time.Second + +// httpnetPingCID is the empty identity block that boxo's bitswap HTTP client +// (bitswap/network/httpnet, var pingCid) GETs to probe whether a provider is +// reachable before fetching real blocks. +const httpnetPingCID = "bafkqaaa" + +// TestHTTPRetrievalRejectsBytesThatDoNotMatchCID checks that when the only +// provider for a CID is an HTTP gateway returning bytes that do not hash to that +// CID, Kubo fails the retrieval instead of returning the corrupt bytes. +// +// Content addressing must hold even when the only source available is broken or +// lying: better a failed fetch than silently corrupt data. +func TestHTTPRetrievalRejectsBytesThatDoNotMatchCID(t *testing.T) { + t.Parallel() + debug := os.Getenv("DEBUG") == "true" + + node, router := newHTTPRetrievalNode(t, debug) + + // Compute the CID of the real content without storing it locally (-n), so + // the daemon must fetch it from the (only) HTTP provider. A small CIDv1 + // input becomes a single raw block whose bytes are exactly the content. + content := uniq("kubo http retrieval rejects bytes that do not match the requested cid") + testCID := cid.MustParse(node.PipeStrToIPFS(content, "add", "-qn", "--cid-version=1").Stdout.Trimmed()) + + // One provider, and it serves bytes that do not match testCID. + corrupt := []byte("these bytes deliberately do not hash to the requested CID") + badProvider := newMockGatewayProvider(t, testCID, corrupt, debug) + badPeerID := registerHTTPProvider(t, router, testCID, badProvider.multiaddr(t)) + + node.StartDaemon() + defer node.StopDaemon() + + // The provider is discoverable, so the daemon has something to try. + findprovs := node.IPFS("routing", "findprovs", testCID.String()) + require.Equal(t, badPeerID.String(), findprovs.Stdout.Trimmed(), + "the broken provider should be discoverable via /routing/v1") + + // Attempt retrieval. The only provider keeps failing verification, so the + // daemon never satisfies the request; --timeout bounds the wait. + res := node.RunIPFS("cat", "--timeout="+httpRetrievalNoLeakTimeout.String(), testCID.String()) + + // It must fail, and crucially it must not emit the corrupt bytes. + require.NotNil(t, res.ExitErr, + "ipfs cat must fail when the only provider serves bytes that do not match the CID (stdout=%q stderr=%q)", + res.Stdout.String(), res.Stderr.String()) + require.Empty(t, res.Stdout.Bytes(), + "ipfs cat must not output anything that failed CID verification") + + // Prove the daemon actually fetched the block from the provider and rejected + // it, rather than simply never reaching it. + require.Positive(t, badProvider.blockGETs.Load(), + "the daemon should have fetched the block from the provider and rejected it on hash mismatch") +} + +// TestHTTPRetrievalFailsOverFromCorruptProvider checks that one bad HTTP +// provider does not poison retrieval: when a CID is served by both a broken +// provider (wrong bytes) and a valid one, Kubo returns the valid bytes. +// +// A single misbehaving provider in a set should not deny the content. Kubo must +// reject its bytes and fetch the real block from a provider that serves it. +func TestHTTPRetrievalFailsOverFromCorruptProvider(t *testing.T) { + t.Parallel() + debug := os.Getenv("DEBUG") == "true" + + node, router := newHTTPRetrievalNode(t, debug) + + content := uniq("kubo http retrieval fails over from a corrupt provider to a valid one") + testCID := cid.MustParse(node.PipeStrToIPFS(content, "add", "-qn", "--cid-version=1").Stdout.Trimmed()) + + // Register the broken provider first, exercising the worst case where it may + // be contacted before the valid one. + corrupt := []byte("these bytes deliberately do not hash to the requested CID") + badProvider := newMockGatewayProvider(t, testCID, corrupt, debug) + registerHTTPProvider(t, router, testCID, badProvider.multiaddr(t)) + + // And a valid provider that serves the real content. + goodProvider := newMockGatewayProvider(t, testCID, []byte(content), debug) + registerHTTPProvider(t, router, testCID, goodProvider.multiaddr(t)) + + node.StartDaemon() + defer node.StopDaemon() + + // Retrieval should succeed with the correct bytes. The generous --timeout is + // a safety bound so a regression surfaces as a failure rather than a hang. + res := node.RunIPFS("cat", "--timeout=60s", testCID.String()) + require.Nil(t, res.ExitErr, + "ipfs cat should succeed via the valid provider (stderr=%q)", res.Stderr.String()) + require.Equal(t, content, res.Stdout.Trimmed(), + "ipfs cat must return the bytes that match the CID, from the valid provider") +} + +// newHTTPRetrievalNode returns an initialized (not yet started) Kubo node wired +// to fetch blocks over HTTP, and the in-process /routing/v1 mock that decides +// which providers it discovers. The caller computes CIDs, registers providers, +// then starts the daemon. +func newHTTPRetrievalNode(t *testing.T, debug bool) (*harness.Node, *httprouting.MockHTTPContentRouter) { + t.Helper() + + router := &httprouting.MockHTTPContentRouter{Debug: debug} + routerSrv := httptest.NewServer(server.Handler(router)) + t.Cleanup(routerSrv.Close) + + node := harness.NewT(t).NewNode().Init() + node.UpdateConfig(func(cfg *config.Config) { + // Fetch blocks directly from HTTP providers found via /routing/v1. + cfg.HTTPRetrieval.Enabled = config.True + // Our mock providers serve over self-signed TLS. + cfg.HTTPRetrieval.TLSInsecureSkipVerify = config.True + // Resolve providers only through the in-process mock router, so the test + // needs no internet and stays deterministic. + cfg.Routing.DelegatedRouters = []string{routerSrv.URL} + }) + return node, router +} + +// registerHTTPProvider advertises srvMultiaddr as a /routing/v1 provider for c +// and returns the peer ID it was given. The record matches what a real HTTP +// gateway publishes (Schema "peer", an /https multiaddr, and the +// transport-ipfs-gateway-http protocol). +func registerHTTPProvider(t *testing.T, router *httprouting.MockHTTPContentRouter, c cid.Cid, srvMultiaddr multiaddr.Multiaddr) peer.ID { + t.Helper() + pid := randomPeerID(t) + router.AddProvider(c, &types.PeerRecord{ + Schema: types.SchemaPeer, + ID: &pid, + Addrs: []types.Multiaddr{{Multiaddr: srvMultiaddr}}, + Protocols: []string{"transport-ipfs-gateway-http"}, + }) + return pid +} + +// randomPeerID returns a valid, unique peer ID. Its value is irrelevant to HTTP +// retrieval (the multiaddr drives the connection); it only needs to be distinct +// so two providers are two separate records. +func randomPeerID(t *testing.T) peer.ID { + t.Helper() + _, pub, err := crypto.GenerateEd25519Key(crand.Reader) + require.NoError(t, err) + pid, err := peer.IDFromPublicKey(pub) + require.NoError(t, err) + return pid +} + +// mockGatewayProvider is a minimal trustless HTTP gateway standing in for an +// HTTP provider discovered via /routing/v1. It answers block requests for one +// target CID (GET/HEAD /ipfs/?format=raw, application/vnd.ipld.raw) with a +// fixed payload, and answers the httpnet connect probe (the empty bafkqaaa +// identity block) so the daemon treats it as reachable. +// +// The payload is set by the caller, so a provider can be told to return bytes +// that do not hash to the target CID, standing in for an HTTP endpoint that +// serves wrong or corrupted data. +type mockGatewayProvider struct { + server *httptest.Server + blockGETs atomic.Int64 // block GETs served, proof the daemon fetched our bytes +} + +func newMockGatewayProvider(t *testing.T, target cid.Cid, payload []byte, debug bool) *mockGatewayProvider { + t.Helper() + p := &mockGatewayProvider{} + blockPath := "/ipfs/" + target.String() + probePath := "/ipfs/" + httpnetPingCID + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if debug { + fmt.Printf("mockGatewayProvider %s %s\n", r.Method, r.URL.Path) + } + switch { + case strings.HasPrefix(r.URL.Path, blockPath): + w.Header().Set("Content-Type", "application/vnd.ipld.raw") + w.WriteHeader(http.StatusOK) + if r.Method == http.MethodGet { + p.blockGETs.Add(1) + _, _ = w.Write(payload) + } + case strings.HasPrefix(r.URL.Path, probePath): + // Reachability probe: any 200 marks the provider connected. + w.Header().Set("Content-Type", "application/vnd.ipld.raw") + w.WriteHeader(http.StatusOK) + default: + http.Error(w, "not found", http.StatusNotFound) + } + }) + + // Self-signed TLS + HTTP/2, matching how real providers serve over https. + // The daemon is configured with HTTPRetrieval.TLSInsecureSkipVerify=true. + srv := httptest.NewUnstartedServer(handler) + srv.EnableHTTP2 = true + srv.StartTLS() + t.Cleanup(srv.Close) + + p.server = srv + return p +} + +// multiaddr returns the provider's address in the shape a real HTTP gateway +// publishes: /ip4//tcp//https. Real records often use +// /dns//tcp/443/https; a loopback IP keeps the test free of any DNS or +// internet dependency while taking the same /https code path. +func (p *mockGatewayProvider) multiaddr(t *testing.T) multiaddr.Multiaddr { + t.Helper() + host, port, err := splitHostPort(p.server.URL) + require.NoError(t, err) + ma, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%s/https", host, port)) + require.NoError(t, err) + return ma +} diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 945ff6075fe..21d7ce804a1 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -181,7 +181,7 @@ require ( github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect - github.com/libp2p/go-libp2p v0.48.0 // indirect + github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-libp2p-kad-dht v0.40.0 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index a0595417533..1e8f8b2c655 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -424,8 +424,8 @@ github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+s github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= -github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286 h1:Y1LIBzKrtU/NbptugPNuem74f355oyWdLRIm0edgozo= +github.com/libp2p/go-libp2p v0.48.1-0.20260603104142-e6309a296286/go.mod h1:+zGTonNiePk+PlraDn51k+8grAbHh9df7IIAVOMwqZo= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-kad-dht v0.40.0 h1:as8U7Y1RX9CTKCBiFBHWKZ6tSS+rE+6WNz+H1+M+wbo=