Add the native Go client and finish M3 hardening

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.
This commit is contained in:
2026-10-26 00:32:31 +08:00
parent cba7d2366b
commit ebae420ab1
27 changed files with 3745 additions and 56 deletions
+18 -2
View File
@@ -72,6 +72,7 @@ type acmeIssuer struct {
store *Store
opts ACMEOptions
sem chan struct{} // bounds concurrent ACME orders
cool cooldown
accountMu sync.Mutex // serializes first-use account registration
}
@@ -85,7 +86,7 @@ func NewACMEIssuer(store *Store, opts ACMEOptions) (Issuer, error) {
if opts.DNS == nil {
return nil, errors.New("acme: a DNS provider is required")
}
return &acmeIssuer{store: store, opts: opts, sem: make(chan struct{}, 2)}, nil
return &acmeIssuer{store: store, opts: opts, sem: make(chan struct{}, 2), cool: newCooldown()}, nil
}
func (a *acmeIssuer) Instant() bool { return false }
@@ -183,18 +184,33 @@ func (a *acmeIssuer) client(ctx context.Context) (*acme.Client, error) {
// Issue orders a certificate for the tunnel's names (hostname and, when the
// CSR asks for it, its wildcard) using one DNS-01 challenge record shared by
// both, then returns the certificate for the client's key.
func (a *acmeIssuer) Issue(ctx context.Context, csrPEM, hostname string) (IssuedCert, error) {
func (a *acmeIssuer) Issue(ctx context.Context, csrPEM, hostname string) (result IssuedCert, err error) {
csr, err := parseCSR(csrPEM, hostname)
if err != nil {
return IssuedCert{}, err
}
if delay := a.cool.remaining(); delay > 0 {
return IssuedCert{}, fmt.Errorf("acme: CA cooling down for %s after repeated failures", delay.Round(time.Second))
}
select {
case a.sem <- struct{}{}:
case <-ctx.Done():
return IssuedCert{}, ctx.Err()
}
defer func() { <-a.sem }()
defer func() {
if err == nil {
a.cool.success()
return
}
if caFailure(err) {
if delay := a.cool.failure(); delay > 0 {
log.Printf("acme: pausing new orders for %s after repeated CA failures", delay)
}
}
}()
client, err := a.client(ctx)
if err != nil {