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,83 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRateLimiter(t *testing.T) {
|
||||
now := time.Now()
|
||||
limiter := NewRateLimiter(2, time.Hour)
|
||||
limiter.now = func() time.Time { return now }
|
||||
|
||||
if ok, _ := limiter.Allow("a"); !ok {
|
||||
t.Fatal("first event should pass")
|
||||
}
|
||||
if ok, _ := limiter.Allow("a"); !ok {
|
||||
t.Fatal("second event should pass")
|
||||
}
|
||||
ok, retry := limiter.Allow("a")
|
||||
if ok || retry <= 0 || retry > time.Hour {
|
||||
t.Fatalf("third event: ok=%v retry=%v", ok, retry)
|
||||
}
|
||||
if ok, _ := limiter.Allow("b"); !ok {
|
||||
t.Fatal("another key should have its own window")
|
||||
}
|
||||
|
||||
now = now.Add(time.Hour + time.Second)
|
||||
if ok, _ := limiter.Allow("a"); !ok {
|
||||
t.Fatal("a new window should allow events again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIP(t *testing.T) {
|
||||
cases := []struct {
|
||||
forwarded, real, remote, want string
|
||||
}{
|
||||
{"203.0.113.9, 10.0.0.1", "", "10.0.0.2:1234", "203.0.113.9"},
|
||||
{"", "198.51.100.7", "10.0.0.2:1234", "198.51.100.7"},
|
||||
{"", "", "192.0.2.5:443", "192.0.2.5"},
|
||||
{"garbage", "", "192.0.2.5:443", "192.0.2.5"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.RemoteAddr = c.remote
|
||||
if c.forwarded != "" {
|
||||
r.Header.Set("X-Forwarded-For", c.forwarded)
|
||||
}
|
||||
if c.real != "" {
|
||||
r.Header.Set("X-Real-IP", c.real)
|
||||
}
|
||||
if got := clientIP(r); got != c.want {
|
||||
t.Errorf("clientIP(%q, %q, %q) = %q, want %q", c.forwarded, c.real, c.remote, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRateLimit(t *testing.T) {
|
||||
store, err := OpenStore("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
api := &API{
|
||||
Store: store, Hub: NewHub(store), Domain: "opentunnel.test", StartedAt: time.Now(),
|
||||
CreateLimiter: NewRateLimiter(1, time.Hour),
|
||||
}
|
||||
srv := httptest.NewServer(api)
|
||||
defer srv.Close()
|
||||
|
||||
resp := request(t, "POST", srv.URL+"/api/tunnel", "", "application/json", `{}`)
|
||||
if resp.StatusCode != 201 {
|
||||
t.Fatalf("first create: %d", resp.StatusCode)
|
||||
}
|
||||
resp = request(t, "POST", srv.URL+"/api/tunnel", "", "application/json", `{}`)
|
||||
body := bodyString(t, resp)
|
||||
if resp.StatusCode != 429 || !strings.Contains(body, `"_tag":"RateLimitedError"`) {
|
||||
t.Fatalf("second create: %d %q", resp.StatusCode, body)
|
||||
}
|
||||
if resp.Header.Get("Retry-After") == "" {
|
||||
t.Error("429 response is missing Retry-After")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user