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:
@@ -0,0 +1,94 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user