tstest/natlab: add ACME cert vmtest

This adds a fake vnet ACME service, TXT-backed SetDNS support, and a
VM test that fetches a certificate with tailscale cert, serves it with
tailscale serve, and verifies HTTPS from a second node.

This adds coverage motivated by #19915.

Updates #13038

Change-Id: Ie1e53409509337d81c8fbceb63f59f3dfbd48207
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-06-05 08:57:17 -07:00
committed by Brad Fitzpatrick
parent 84ffcd2759
commit 26864f1302
7 changed files with 818 additions and 6 deletions
+57 -4
View File
@@ -445,6 +445,17 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
return
}
if destPort == 80 && fakeACME.Match(destIP) && n.s.fakeACME != nil {
r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep)
context.AfterFunc(n.s.shutdownCtx, func() { tc.SetDeadline(time.Now()) })
hs := &http.Server{Handler: n.s.fakeACME}
n.s.wg.Go(func() {
hs.Serve(netutil.NewOneConnListener(tc, nil))
})
return
}
var targetDial string
if n.s.derpIPs.Contains(destIP) {
targetDial = destIP.String() + ":" + strconv.Itoa(int(destPort))
@@ -831,6 +842,7 @@ type Server struct {
control *testcontrol.Server
derps []*derpServer
fakeACME *fakeACMEServer
pcapWriter *pcapWriter
// writeMu serializes all writes to VM clients.
@@ -845,6 +857,7 @@ type Server struct {
cloudInitData map[int]*CloudInitData // node num → cloud-init config
fileContents map[string][]byte // filename → file bytes
dnsTXTRecords map[string][]string
// onDHCPEvent, if non-nil, is called when DHCP messages are processed.
// Parameters are: source MAC, node number, DHCP message type, assigned IP.
@@ -922,14 +935,21 @@ func New(c *Config) (*Server, error) {
DERPMap: derpMap,
ExplicitBaseURL: "http://control.tailscale",
},
fakeACME: newFakeACMEServer("http://acme.example"),
blendReality: c.blendReality,
derpIPs: set.Of[netip.Addr](),
nodeByMAC: map[MAC]*node{},
networkByWAN: &bart.Table[*network]{},
networks: set.Of[*network](),
nodeByMAC: map[MAC]*node{},
networkByWAN: &bart.Table[*network]{},
networks: set.Of[*network](),
dnsTXTRecords: map[string][]string{},
}
s.control.OnSetDNS = func(req *tailcfg.SetDNSRequest) error {
s.setDNSRecord(req.Name, req.Value)
return nil
}
s.fakeACME.lookupTXT = s.lookupTXT
for _, host := range derpHostnames {
s.derps = append(s.derps, newDERPServer(host))
}
@@ -968,6 +988,28 @@ func (s *Server) DERPCertSHA256Hex(idx int) string {
return s.derps[idx].certSHA256Hex
}
// FakeACMEDirectoryURL returns the directory URL for vnet's in-process ACME CA.
func (s *Server) FakeACMEDirectoryURL() string {
return s.fakeACME.directoryURL()
}
// FakeACMERootPEM returns the PEM-encoded root certificate for vnet's fake ACME CA.
func (s *Server) FakeACMERootPEM() []byte {
return s.fakeACME.rootPEM()
}
func (s *Server) setDNSRecord(name, value string) {
s.mu.Lock()
defer s.mu.Unlock()
s.dnsTXTRecords[name] = append(s.dnsTXTRecords[name], value)
}
func (s *Server) lookupTXT(name string) []string {
s.mu.Lock()
defer s.mu.Unlock()
return append([]string(nil), s.dnsTXTRecords[name]...)
}
// CloudInitData holds the cloud-init configuration for a node.
type CloudInitData struct {
MetaData string
@@ -2270,7 +2312,7 @@ func (s *Server) shouldInterceptTCP(pkt gopacket.Packet) bool {
}
if tcp.DstPort == 80 || tcp.DstPort == 443 {
for _, v := range []virtualIP{fakeControl, fakeDERP1, fakeDERP2, fakeLogCatcher, fakeCloudInit, fakeFiles} {
for _, v := range []virtualIP{fakeControl, fakeDERP1, fakeDERP2, fakeLogCatcher, fakeCloudInit, fakeFiles, fakeACME} {
if v.Match(flow.dst) {
return true
}
@@ -2399,6 +2441,17 @@ func (s *Server) createDNSResponse(pkt gopacket.Packet) ([]byte, error) {
TTL: 60,
})
}
} else if q.Type == layers.DNSTypeTXT {
for _, txt := range s.lookupTXT(string(q.Name)) {
response.ANCount++
response.Answers = append(response.Answers, layers.DNSResourceRecord{
Name: q.Name,
Type: q.Type,
Class: q.Class,
TXTs: [][]byte{[]byte(txt)},
TTL: 60,
})
}
}
}