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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user