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
+32
View File
@@ -60,10 +60,24 @@ type adminOverview struct {
Server adminServer `json:"server"`
CA adminCA `json:"ca"`
Totals adminTotals `json:"totals"`
Metrics adminMetrics `json:"metrics"`
Tunnels []adminTunnel `json:"tunnels"`
Bridges []adminBridge `json:"bridges"`
}
type adminMetrics struct {
CreateRequests uint64 `json:"create_requests"`
BindRequests uint64 `json:"bind_requests"`
RateLimited uint64 `json:"rate_limited"`
IssuanceAttempts uint64 `json:"issuance_attempts"`
IssuanceReady uint64 `json:"issuance_ready"`
IssuanceFailed uint64 `json:"issuance_failed"`
CertificatesReady int `json:"certificates_ready"`
CertificatesIssuing int `json:"certificates_issuing"`
CertificatesFailed int `json:"certificates_failed"`
CertificatesRetrying int `json:"certificates_retrying"`
}
// adminAuthorized enforces the bearer ADMIN_TOKEN; 503 when unset.
func (a *API) adminAuthorized(w http.ResponseWriter, r *http.Request) bool {
if a.AdminToken == "" {
@@ -145,10 +159,28 @@ func (a *API) adminOverviewHandler(w http.ResponseWriter, r *http.Request) {
entry.CertificateState = certState(t.Certificate).Type
entry.CertificateIssuer = t.Certificate.Issuer
entry.CertificateError = t.Certificate.Reason
switch {
case certReady(t.Certificate):
overview.Metrics.CertificatesReady++
if t.Certificate.State == "failed" {
overview.Metrics.CertificatesRetrying++
}
case t.Certificate.State == "failed":
overview.Metrics.CertificatesFailed++
default:
overview.Metrics.CertificatesIssuing++
}
}
overview.Tunnels = append(overview.Tunnels, entry)
}
overview.Metrics.CreateRequests = a.createCount.Load()
overview.Metrics.BindRequests = a.bindCount.Load()
overview.Metrics.RateLimited = a.rateLimited.Load()
overview.Metrics.IssuanceAttempts = a.issueAttempts.Load()
overview.Metrics.IssuanceReady = a.issueReady.Load()
overview.Metrics.IssuanceFailed = a.issueFailed.Load()
writeJSON(w, http.StatusOK, overview)
}