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.
65 lines
1.5 KiB
Go
65 lines
1.5 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/acme"
|
|
)
|
|
|
|
func TestCooldown(t *testing.T) {
|
|
now := time.Now()
|
|
c := newCooldown()
|
|
c.now = func() time.Time { return now }
|
|
|
|
for i := 0; i < 4; i++ {
|
|
if d := c.failure(); d != 0 {
|
|
t.Fatalf("failure %d paused for %v, want no pause", i+1, d)
|
|
}
|
|
}
|
|
if d := c.failure(); d != time.Minute {
|
|
t.Fatalf("fifth failure pause = %v, want 1m", d)
|
|
}
|
|
if d := c.remaining(); d != time.Minute {
|
|
t.Fatalf("remaining = %v, want 1m", d)
|
|
}
|
|
if d := c.failure(); d != 2*time.Minute {
|
|
t.Fatalf("sixth failure pause = %v, want 2m", d)
|
|
}
|
|
for i := 0; i < 20; i++ {
|
|
c.failure()
|
|
}
|
|
if d := c.failure(); d != time.Hour {
|
|
t.Fatalf("pause cap = %v, want 1h", d)
|
|
}
|
|
|
|
now = now.Add(2 * time.Hour)
|
|
if d := c.remaining(); d != 0 {
|
|
t.Fatalf("remaining after expiry = %v", d)
|
|
}
|
|
c.success()
|
|
if c.failures != 0 {
|
|
t.Fatalf("success left failures = %d", c.failures)
|
|
}
|
|
}
|
|
|
|
func TestCAFailureClassification(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
want bool
|
|
}{
|
|
{errors.New("dns: create _acme-challenge: nope"), false},
|
|
{errors.New("acme: authorization for x is invalid"), false},
|
|
{errors.New("acme: authorize order: context deadline exceeded"), true},
|
|
{fmt.Errorf("acme: authorize order: %w", &acme.Error{StatusCode: 503}), true},
|
|
{fmt.Errorf("acme: authorize order: %w", &acme.Error{StatusCode: 429}), true},
|
|
}
|
|
for _, c := range cases {
|
|
if got := caFailure(c.err); got != c.want {
|
|
t.Errorf("caFailure(%v) = %v, want %v", c.err, got, c.want)
|
|
}
|
|
}
|
|
}
|