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.
95 lines
1.9 KiB
Go
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
|
|
}
|