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,581 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.tzgyn.dev/TZGyn/tunnel/internal/client"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:]))
|
||||
}
|
||||
|
||||
func run(args []string) int {
|
||||
configPath := client.DefaultPath()
|
||||
if env := os.Getenv("TUNNEL_CONFIG"); env != "" {
|
||||
configPath = env
|
||||
}
|
||||
fs := flag.NewFlagSet("tunnel", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
fs.StringVar(&configPath, "config", configPath, "config file (default ~/.config/tunnel/config.json)")
|
||||
fs.Usage = func() { usage(fs.Output()) }
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
rest := fs.Args()
|
||||
if len(rest) == 0 {
|
||||
usage(os.Stderr)
|
||||
return 2
|
||||
}
|
||||
command, rest := rest[0], rest[1:]
|
||||
switch command {
|
||||
case "config":
|
||||
return runConfig(configPath, rest)
|
||||
case "route":
|
||||
return runRoute(configPath, rest)
|
||||
case "import":
|
||||
return runImport(configPath, rest)
|
||||
case "up":
|
||||
return runUp(configPath)
|
||||
case "down":
|
||||
return runDown(configPath)
|
||||
case "serve":
|
||||
return runServe(configPath)
|
||||
case "status":
|
||||
return runStatus(configPath)
|
||||
case "logs":
|
||||
return runLogs(configPath)
|
||||
case "version":
|
||||
fmt.Println("tunnel", version)
|
||||
return 0
|
||||
case "help", "-h", "--help":
|
||||
usage(os.Stdout)
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", command)
|
||||
usage(os.Stderr)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func usage(w io.Writer) {
|
||||
fmt.Fprint(w, `tunnel - OpenTunnel client for a self-hosted server
|
||||
|
||||
Usage:
|
||||
tunnel [--config FILE] <command> [options]
|
||||
|
||||
Commands:
|
||||
config set-api <url> Store the server API URL in the config
|
||||
config show Show the config (never the token)
|
||||
route add <port|host:port> [--name NAME] [--proxy-protocol v1|v2]
|
||||
route remove <name>
|
||||
route list
|
||||
import --profile NAME --api URL
|
||||
Take over an existing official opentunnel profile
|
||||
up Provision if needed and run in the background
|
||||
down Stop the background service
|
||||
serve Run in the foreground (debugging)
|
||||
status Show the tunnel, certificate, routes and daemon
|
||||
logs Print the end of the daemon log
|
||||
|
||||
Examples:
|
||||
tunnel config set-api https://tunnel.tzgyn.dev
|
||||
tunnel route add 4096 --name api
|
||||
tunnel up
|
||||
`)
|
||||
}
|
||||
|
||||
func runConfig(configPath string, args []string) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel config set-api <url> | tunnel config show")
|
||||
return 2
|
||||
}
|
||||
switch args[0] {
|
||||
case "set-api", "set":
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel config set-api <url>")
|
||||
return 2
|
||||
}
|
||||
api, err := client.NormalizeAPI(args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
old := cfg.API
|
||||
cfg.API = api
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("API set to %s\n", api)
|
||||
if old != "" && old != api && cfg.TunnelID != "" {
|
||||
fmt.Println("Note: the saved tunnel identity was kept; if that tunnel does not exist on this server, remove the config directory and start again.")
|
||||
}
|
||||
return 0
|
||||
case "show":
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
printConfig(configPath, cfg)
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel config set-api <url> | tunnel config show")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func printConfig(configPath string, cfg *client.Config) {
|
||||
fmt.Printf("Config: %s\n", configPath)
|
||||
fmt.Printf("API: %s\n", valueOr(cfg.API, "(not set)"))
|
||||
fmt.Printf("Tunnel: %s\n", valueOr(cfg.TunnelID, "(not created)"))
|
||||
fmt.Printf("Hostname: %s\n", valueOr(cfg.Hostname, "(not created)"))
|
||||
fmt.Printf("Cert: %s\n", valueOr(cfg.CertificateExpiry, "(not issued)"))
|
||||
if len(cfg.Routes) == 0 {
|
||||
fmt.Println("Routes: (none)")
|
||||
return
|
||||
}
|
||||
fmt.Println("Routes:")
|
||||
for _, name := range sortedRouteNames(cfg) {
|
||||
route := cfg.Routes[name]
|
||||
fmt.Printf(" %-20s -> %-24s %s\n", name, route.Target, cfg.RouteURL(name))
|
||||
}
|
||||
}
|
||||
|
||||
func runRoute(configPath string, args []string) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel route add|remove|list")
|
||||
return 2
|
||||
}
|
||||
switch args[0] {
|
||||
case "add":
|
||||
var target, name, proxy string
|
||||
for i := 1; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--name":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "--name needs a value")
|
||||
return 2
|
||||
}
|
||||
i++
|
||||
name = args[i]
|
||||
case strings.HasPrefix(arg, "--name="):
|
||||
name = strings.TrimPrefix(arg, "--name=")
|
||||
case arg == "--proxy-protocol":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "--proxy-protocol needs v1 or v2")
|
||||
return 2
|
||||
}
|
||||
i++
|
||||
proxy = args[i]
|
||||
case strings.HasPrefix(arg, "--proxy-protocol="):
|
||||
proxy = strings.TrimPrefix(arg, "--proxy-protocol=")
|
||||
case strings.HasPrefix(arg, "-"):
|
||||
fmt.Fprintf(os.Stderr, "unknown option %s\n", arg)
|
||||
return 2
|
||||
case target == "":
|
||||
target = arg
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unexpected argument %q\n", arg)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel route add <port|host:port> [--name NAME]")
|
||||
return 2
|
||||
}
|
||||
normalized, err := client.NormalizeTarget(target)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if name == "" {
|
||||
name = randomRouteName()
|
||||
}
|
||||
if !client.ValidRouteName(name) {
|
||||
fmt.Fprintf(os.Stderr, "invalid route name %q\n", name)
|
||||
return 1
|
||||
}
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if cfg.API == "" {
|
||||
fmt.Fprintln(os.Stderr, "no API configured; run: tunnel config set-api https://your-server")
|
||||
return 1
|
||||
}
|
||||
svc, err := client.NewService(configPath, version, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
err = svc.Ensure(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "provision tunnel: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
cfg = svc.Config
|
||||
if err := cfg.AddRoute(name, normalized, proxy); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
fmt.Println("Tunnel ready.")
|
||||
fmt.Printf("Route %s -> %s\n", name, cfg.RouteURL(name))
|
||||
if err := startDaemon(configPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "service: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "remove":
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel route remove <name>")
|
||||
return 2
|
||||
}
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if !cfg.RemoveRoute(args[1]) {
|
||||
fmt.Fprintf(os.Stderr, "no route named %q\n", args[1])
|
||||
return 1
|
||||
}
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("Removed route %s (the service reloads within a few seconds).\n", args[1])
|
||||
return 0
|
||||
case "list":
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if len(cfg.Routes) == 0 {
|
||||
fmt.Println("No routes.")
|
||||
return 0
|
||||
}
|
||||
for _, name := range sortedRouteNames(cfg) {
|
||||
route := cfg.Routes[name]
|
||||
fmt.Printf("%-20s -> %-24s %s\n", name, route.Target, cfg.RouteURL(name))
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel route add|remove|list")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func runImport(configPath string, args []string) int {
|
||||
fs := flag.NewFlagSet("import", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
var profile, api string
|
||||
var force bool
|
||||
fs.StringVar(&profile, "profile", "default", "official opentunnel profile name")
|
||||
fs.StringVar(&api, "api", "", "server API URL")
|
||||
fs.BoolVar(&force, "force", false, "overwrite an existing config")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
result, err := client.Import(client.ImportOptions{
|
||||
Profile: profile,
|
||||
API: api,
|
||||
ConfigPath: configPath,
|
||||
Force: force,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
sort.Strings(result.Routes)
|
||||
fmt.Printf("Imported %s (%s) from %s\n", result.TunnelID, result.Hostname, result.Source)
|
||||
fmt.Printf("Config: %s\n", result.ConfigPath)
|
||||
if len(result.Routes) == 0 {
|
||||
fmt.Println("Routes: (none)")
|
||||
} else {
|
||||
fmt.Printf("Routes: %s\n", strings.Join(result.Routes, ", "))
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Stop the official service before starting this one:")
|
||||
fmt.Printf(" opentunnel --profile %s down\n", profile)
|
||||
fmt.Println("Then run:")
|
||||
fmt.Printf(" tunnel --config %s up\n", result.ConfigPath)
|
||||
return 0
|
||||
}
|
||||
|
||||
func runUp(configPath string) int {
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if cfg.API == "" {
|
||||
fmt.Fprintln(os.Stderr, "no API configured; run: tunnel config set-api https://your-server")
|
||||
return 1
|
||||
}
|
||||
if err := startDaemon(configPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func startDaemon(configPath string) error {
|
||||
if pid, err := readPID(client.ConfigPaths(configPath).PID); err == nil && processAlive(pid) {
|
||||
fmt.Printf("Already running (pid %d).\n", pid)
|
||||
return nil
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths := client.ConfigPaths(configPath)
|
||||
if err := os.MkdirAll(paths.Dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
logFile, err := os.OpenFile(paths.Log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(exe, "--config", configPath, "serve")
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.Stdin = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return err
|
||||
}
|
||||
logFile.Close()
|
||||
pid := cmd.Process.Pid
|
||||
_ = cmd.Process.Release()
|
||||
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
pidOnDisk, err := readPID(paths.PID)
|
||||
if err == nil && pidOnDisk == pid && processAlive(pid) {
|
||||
fmt.Printf("Started (pid %d). Log: %s\n", pid, paths.Log)
|
||||
return nil
|
||||
}
|
||||
if !processAlive(pid) {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if processAlive(pid) {
|
||||
fmt.Printf("Started (pid %d). Log: %s\n", pid, paths.Log)
|
||||
return nil
|
||||
}
|
||||
tailLog(paths.Log, 20)
|
||||
return errors.New("service exited during startup; see the log above")
|
||||
}
|
||||
|
||||
func runDown(configPath string) int {
|
||||
paths := client.ConfigPaths(configPath)
|
||||
pid, err := readPID(paths.PID)
|
||||
if err != nil {
|
||||
fmt.Println("Not running.")
|
||||
return 0
|
||||
}
|
||||
if !processAlive(pid) {
|
||||
_ = os.Remove(paths.PID)
|
||||
fmt.Println("Not running (removed stale pid file).")
|
||||
return 0
|
||||
}
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "stop pid %d: %v\n", pid, err)
|
||||
return 1
|
||||
}
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if !processAlive(pid) {
|
||||
_ = os.Remove(paths.PID)
|
||||
fmt.Printf("Stopped (pid %d).\n", pid)
|
||||
return 0
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "pid %d did not stop within 5s\n", pid)
|
||||
return 1
|
||||
}
|
||||
|
||||
func runServe(configPath string) int {
|
||||
paths := client.ConfigPaths(configPath)
|
||||
if pid, err := readPID(paths.PID); err == nil && pid != os.Getpid() && processAlive(pid) {
|
||||
fmt.Fprintf(os.Stderr, "already running (pid %d)\n", pid)
|
||||
return 1
|
||||
}
|
||||
svc, err := client.NewService(configPath, version, log.New(os.Stderr, "", log.LstdFlags))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if err := writePID(paths.PID); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
defer func() {
|
||||
if pid, err := readPID(paths.PID); err == nil && pid == os.Getpid() {
|
||||
_ = os.Remove(paths.PID)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if err := svc.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
svc.Log.Printf("service stopped: %v", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runStatus(configPath string) int {
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
printConfig(configPath, cfg)
|
||||
paths := client.ConfigPaths(configPath)
|
||||
if pid, err := readPID(paths.PID); err == nil && processAlive(pid) {
|
||||
fmt.Printf("Service: running (pid %d)\n", pid)
|
||||
} else {
|
||||
fmt.Println("Service: stopped")
|
||||
}
|
||||
if cfg.API == "" || cfg.TunnelID == "" || cfg.Token == "" {
|
||||
return 0
|
||||
}
|
||||
svc, err := client.NewService(configPath, version, nil)
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
defer cancel()
|
||||
info, err := svc.API.GetTunnel(ctx, cfg.TunnelID, cfg.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("Server: error: %v\n", err)
|
||||
return 0
|
||||
}
|
||||
fmt.Printf("Server: %s\n", info.State)
|
||||
cert, err := svc.API.GetCertificate(ctx, cfg.TunnelID, cfg.Token)
|
||||
if err == nil {
|
||||
line := cert.State.Type
|
||||
if cert.State.Type == "ready" {
|
||||
line += " (" + cert.State.Expiry + ")"
|
||||
}
|
||||
if cert.State.Reason != "" {
|
||||
line += ": " + cert.State.Reason
|
||||
}
|
||||
fmt.Printf("Cert state: %s\n", line)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runLogs(configPath string) int {
|
||||
paths := client.ConfigPaths(configPath)
|
||||
if _, err := os.Stat(paths.Log); err != nil {
|
||||
fmt.Println("No log yet:", paths.Log)
|
||||
return 0
|
||||
}
|
||||
fmt.Println("Log:", paths.Log)
|
||||
tailLog(paths.Log, 100)
|
||||
return 0
|
||||
}
|
||||
|
||||
func sortedRouteNames(cfg *client.Config) []string {
|
||||
names := make([]string, 0, len(cfg.Routes))
|
||||
for name := range cfg.Routes {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func writePID(path string) error {
|
||||
return os.WriteFile(path, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o600)
|
||||
}
|
||||
|
||||
func readPID(path string) (int, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
if err != nil || pid <= 0 {
|
||||
return 0, errors.New("invalid pid file")
|
||||
}
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
func processAlive(pid int) bool {
|
||||
err := syscall.Kill(pid, 0)
|
||||
return err == nil || errors.Is(err, syscall.EPERM)
|
||||
}
|
||||
|
||||
func tailLog(path string, lines int) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(file, 1<<20))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
all := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
||||
if len(all) > lines {
|
||||
all = all[len(all)-lines:]
|
||||
}
|
||||
for _, line := range all {
|
||||
if line != "" {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomRouteName() string {
|
||||
buf := make([]byte, 10)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "route"
|
||||
}
|
||||
return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(buf))
|
||||
}
|
||||
|
||||
func valueOr(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -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