Files
TZGyn ebae420ab1 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.
2026-10-26 00:32:31 +08:00

95 lines
1.9 KiB
Go

package server
import (
"errors"
"net"
"strings"
"sync"
"time"
"golang.org/x/crypto/acme"
)
// cooldown pauses new ACME orders after repeated CA-side failures. Five
// consecutive failures trigger a one-minute pause, doubling up to an hour; a
// success clears the state.
type cooldown struct {
mu sync.Mutex
failures int
until time.Time
now func() time.Time
}
func newCooldown() cooldown {
return cooldown{now: time.Now}
}
func (c *cooldown) remaining() time.Duration {
c.mu.Lock()
defer c.mu.Unlock()
if c.until.IsZero() {
return 0
}
now := c.now()
if !now.Before(c.until) {
return 0
}
return c.until.Sub(now)
}
func (c *cooldown) success() {
c.mu.Lock()
c.failures = 0
c.until = time.Time{}
c.mu.Unlock()
}
// failure records a CA-side failure and returns the new pause, if one began.
func (c *cooldown) failure() time.Duration {
c.mu.Lock()
defer c.mu.Unlock()
c.failures++
if c.failures < 5 {
return 0
}
exponent := c.failures - 5
if exponent > 6 {
exponent = 6 // cap at one hour before shifting
}
delay := time.Minute << exponent
if delay > time.Hour {
delay = time.Hour
}
c.until = c.now().Add(delay)
return delay
}
// caFailure distinguishes failures that point at the CA (network, 5xx, rate
// limits) from local DNS/CSR problems and validation errors, which should not
// pause issuance.
func caFailure(err error) bool {
if err == nil {
return false
}
var hostErr errCSRHostname
if errors.As(err, &hostErr) {
return false
}
text := err.Error()
if strings.Contains(text, "dns:") || strings.Contains(text, "not a PEM") || strings.Contains(text, "CSR") {
return false
}
if strings.Contains(text, "authorization") || strings.Contains(text, "dns-01") {
return false
}
var netErr net.Error
if errors.As(err, &netErr) {
return true
}
var acmeErr *acme.Error
if errors.As(err, &acmeErr) {
return acmeErr.StatusCode == 429 || acmeErr.StatusCode >= 500
}
return true
}