The new cmd/tunnel client keeps the API URL and tunnel identity in ~/.config/tunnel/config.json (0600), provisions the key/CSR and certificate, terminates TLS, proxies to local routes, reloads routes without a restart, and runs as a background service. `tunnel import` takes over an existing official CLI profile, preserving its tunnel ID, token, key and certificate. Server hardening: per-IP and per-tunnel rate limits for creates and binds, a CA cool-down after repeated ACME failures, a startup sweep for stale _acme-challenge records, admin metrics, a 60-second half-close timeout, and separate prioritized control / ordered data bridge queues (with `end` ordered after the final bytes). The dashboard shows the new metrics.
50 lines
1.6 KiB
Go
50 lines
1.6 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// DNSProvider manages the TXT records ACME DNS-01 challenges need.
|
|
type DNSProvider interface {
|
|
// CreateTXT adds a TXT record with the given value at name.
|
|
CreateTXT(ctx context.Context, name, value string) error
|
|
// DeleteTXT removes every TXT record at name (best effort cleanup).
|
|
DeleteTXT(ctx context.Context, name string) error
|
|
}
|
|
|
|
// ChallengeSweeper is implemented by providers that can remove leftover
|
|
// challenge records at startup (for example after a process was killed
|
|
// mid-issuance).
|
|
type ChallengeSweeper interface {
|
|
SweepChallenges(ctx context.Context) (int, error)
|
|
}
|
|
|
|
// challengeRecordName is the TXT record name a hostname's DNS-01 challenge
|
|
// lives at; the wildcard of a hostname shares the same record.
|
|
func challengeRecordName(hostname string) string {
|
|
return "_acme-challenge." + hostname
|
|
}
|
|
|
|
// relativeRecordName strips the zone suffix from a record name, as DNS
|
|
// provider APIs expect names relative to the zone.
|
|
func relativeRecordName(name, zone string) (string, error) {
|
|
suffix := "." + zone
|
|
if !strings.HasSuffix(name, suffix) {
|
|
return "", fmt.Errorf("record %q is not under zone %q", name, zone)
|
|
}
|
|
return strings.TrimSuffix(name, suffix), nil
|
|
}
|
|
|
|
// dnsZone guesses the registrable zone that hosts the tunnel hostnames:
|
|
// the domain minus its first label when it has three or more (tunnel.tzgyn.dev
|
|
// is served by the tzgyn.dev zone). Override with DNS_ZONE when wrong.
|
|
func DNSZone(domain string) string {
|
|
labels := strings.Split(domain, ".")
|
|
if len(labels) >= 3 {
|
|
return strings.Join(labels[1:], ".")
|
|
}
|
|
return domain
|
|
}
|