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:
@@ -69,6 +69,9 @@ func main() {
|
||||
porkbunSecret := flag.String("porkbun-secret-api-key", envOr("PORKBUN_SECRET_API_KEY", ""), "Porkbun secret API key")
|
||||
dnsZone := flag.String("dns-zone", envOr("DNS_ZONE", ""), "zone hosting the challenge records (default: derived from the domain)")
|
||||
challtestsrvURL := flag.String("challtestsrv-url", envOr("CHALLTESTSRV_URL", ""), "pebble-challtestsrv management URL for DNS_PROVIDER=challtestsrv")
|
||||
createLimit := flag.Int("create-limit-per-hour", envInt("TUNNEL_CREATE_LIMIT_PER_HOUR", 60), "tunnels one client address may create per hour; 0 disables")
|
||||
bindLimit := flag.Int("bind-limit-per-hour", envInt("CERT_BIND_LIMIT_PER_HOUR", 30), "new certificate binds one client address may request per hour; 0 disables")
|
||||
bindTunnelLimit := flag.Int("bind-limit-per-tunnel-per-hour", envInt("CERT_BIND_LIMIT_PER_TUNNEL_PER_HOUR", 10), "new certificate binds one tunnel may request per hour; 0 disables")
|
||||
flag.Parse()
|
||||
|
||||
if *caFile == "" {
|
||||
@@ -117,6 +120,16 @@ func main() {
|
||||
case *dnsProvider != "":
|
||||
log.Fatalf("unknown DNS_PROVIDER %q (want porkbun or challtestsrv)", *dnsProvider)
|
||||
}
|
||||
if sweeper, ok := dns.(server.ChallengeSweeper); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
count, err := sweeper.SweepChallenges(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("dns: stale challenge sweep removed %d record(s), then failed: %v", count, err)
|
||||
} else if count > 0 {
|
||||
log.Printf("dns: removed %d stale challenge record(s)", count)
|
||||
}
|
||||
}
|
||||
if dns != nil {
|
||||
acme, err := server.NewACMEIssuer(store, server.ACMEOptions{
|
||||
DirectoryURL: *acmeDirectory,
|
||||
@@ -144,12 +157,19 @@ func main() {
|
||||
Version: version,
|
||||
Issuer: issuer,
|
||||
IssuerName: issuerName,
|
||||
|
||||
CreateLimiter: server.NewRateLimiter(*createLimit, time.Hour),
|
||||
BindLimiter: server.NewRateLimiter(*bindLimit, time.Hour),
|
||||
BindTunnelLimiter: server.NewRateLimiter(*bindTunnelLimit, time.Hour),
|
||||
}
|
||||
if *adminToken == "" {
|
||||
log.Printf("admin dashboard: disabled (set ADMIN_TOKEN to enable it at /)")
|
||||
} else {
|
||||
log.Printf("admin dashboard: enabled at /")
|
||||
}
|
||||
if *createLimit > 0 || *bindLimit > 0 || *bindTunnelLimit > 0 {
|
||||
log.Printf("limits: %d creates/hour/IP, %d binds/hour/IP, %d binds/hour/tunnel", *createLimit, *bindLimit, *bindTunnelLimit)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
Reference in New Issue
Block a user