Files
tailscale/tstest/natlab/vmtest/selfsignedderp_test.go
T
Brad FitzpatrickandNuke c91b7188e8 ipn/localapi,tstest/natlab: fix debug derp TLS check for sha256-raw CertName
serveDebugDERPRegion built its TLS config with
ServerName: cmp.Or(derpNode.CertName, derpNode.HostName), which for a
"sha256-raw:<hex>" CertName passed the raw fingerprint to Go's stock
verifier as a hostname; the handshake always failed with a hostname
mismatch. This is the second half of #15579; the first half (tailscaled
itself failing with "unexpected multiple certs presented") was fixed in

Extract a tlsConfigForNode helper that mirrors derphttp.Client.tlsClient
so that sha256-raw and domain-fronting CertName values are dispatched
to tlsdial.SetConfigExpectedCertHash and tlsdial.SetConfigExpectedCert
respectively, falling back to HostName when CertName is empty.

The core fix here was originally written by @imnuke in #19965; that PR
also added a unit test in ipn/localapi/debugderp_test.go which is
replaced in this commit by a new vmtest that exercises the whole stack:
vnet now serves a self-signed cert valid for each fake DERP node's
HostName and exposes its SHA-256 fingerprint, and vmtest grows a new
SelfSignedDERPCertPinning EnvOption that swaps the test DERP map's
nodes to CertName="sha256-raw:<hex>" with InsecureForTests cleared.
TestSelfSignedDERPHashPinning then stands up two hard-NAT'd nodes, has
them communicate over DERP, and calls DebugDERPRegion on each. Before
this fix the test fails with the exact x509 hostname-mismatch error
from the original bug; after, it passes.

Updates #15579

Change-Id: I61f38ffebc7ac5abc962639db1ae88f5cd8633b1
Co-authored-by: Nuke <nuke@imnuke.dev>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-06-02 12:02:40 -07:00

61 lines
2.1 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package vmtest_test
import (
"context"
"strings"
"testing"
"time"
"tailscale.com/tstest/natlab/vmtest"
)
// TestSelfSignedDERPHashPinning exercises the sha256-raw DERP cert pinning
// code path end-to-end: tailscaled connects to its home DERP whose cert is
// self-signed and pinned via CertName="sha256-raw:<hex>" (no separate
// fronting CertName), the two nodes communicate over the resulting tailnet,
// and `tailscale debug derp` against the same region succeeds.
//
// Both nodes sit behind hard NATs with no port mapping available so disco
// cannot punch a direct path and the tailnet ping must traverse DERP, making
// the sha256-raw pinning of the tailscaled→DERP path part of the assertion.
//
// The debug-derp half is the regression test for the bug fixed in PR #19965:
// before that change, [ipn/localapi.serveDebugDERPRegion] passed the raw
// sha256-raw fingerprint as the TLS ServerName and the handshake always
// failed with a hostname mismatch.
func TestSelfSignedDERPHashPinning(t *testing.T) {
env := vmtest.New(t, vmtest.SelfSignedDERPCertPinning())
n1 := hard(env)
n2 := hard(env)
env.Start()
if err := env.PingExpect(n1, n2, vmtest.PingRouteDERP, 60*time.Second); err != nil {
t.Fatalf("ping node-0 -> node-1: %v", err)
}
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
for _, n := range []*vmtest.Node{n1, n2} {
rep, err := n.Agent().DebugDERPRegion(ctx, "1")
if err != nil {
t.Fatalf("[%s] DebugDERPRegion(1): %v", n.Name(), err)
}
t.Logf("[%s] DebugDERPRegion(1): info=%v warnings=%v errors=%v",
n.Name(), rep.Info, rep.Warnings, rep.Errors)
for _, e := range rep.Errors {
// The `hard` builder gives the node only an IPv4 LAN, so the
// DebugDERPRegion IPv6 probe predictably fails with
// "network is unreachable". That's orthogonal to the TLS
// verification path this test exists to cover.
if strings.Contains(e, "over IPv6") {
continue
}
t.Errorf("[%s] DebugDERPRegion(1) error: %s", n.Name(), e)
}
}
}