From bcc5e69094844c30f770373ead49172b29c97ee5 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 24 Sep 2026 18:05:20 -0700 Subject: [PATCH] Listen on 0.0.0.0 by default and log LAN addresses - New CANON_HOST (default 0.0.0.0; 127.0.0.1 keeps it on one machine) - On start, log the http:// addresses other devices can use, skipping Docker/VM bridge interfaces - docker-compose publishes 0.0.0.0:4242 explicitly - README: opening Canon from other devices and firewall note Co-Authored-By: Claude Opus 5.5 --- .env.example | 1 + README.md | 5 ++++ cmd/canon/main.go | 48 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 4 +++- internal/config/config.go | 10 +++++++- 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 662b7e3..423eda0 100644 --- a/.env.example +++ b/.env.example @@ -17,5 +17,6 @@ CANON_IGDB_CLIENT_SECRET= # CANON_TRUST_PROXY=true # honor X-Forwarded-For / -Proto / -Host # CANON_SECURE_COOKIES=true # always mark the session cookie Secure +# CANON_HOST=0.0.0.0 # all network interfaces; 127.0.0.1 = this machine only # CANON_PORT=4242 # CANON_LOG_LEVEL=debug diff --git a/README.md b/README.md index 6cb1fc0..ff55222 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ Open `http://localhost:4242/setup`, enter the setup code and create your account To skip the setup page, set `CANON_ADMIN_USERNAME` and `CANON_ADMIN_PASSWORD` in `.env` before the first start. They're only used when no account exists yet. +### Opening it from other devices + +Canon listens on all network interfaces (`0.0.0.0`) by default, so phones and other computers on your network can open `http://:4242`. When run outside Docker, the startup log prints the exact addresses. If another device can't connect, the host's firewall is probably blocking the port (for example `sudo ufw allow 4242/tcp`). To keep Canon private to one machine, set `CANON_HOST=127.0.0.1`, or publish `127.0.0.1:4242:4242` in `docker-compose.yml`. + ### Without Compose ```sh @@ -60,6 +64,7 @@ Keys entered in the UI are stored in the SQLite database on your server. Keys se | Variable | Default | | |---|---|---| +| `CANON_HOST` | `0.0.0.0` | Interface to listen on. `0.0.0.0` makes it reachable from other devices on your network; `127.0.0.1` keeps it on this machine | | `CANON_PORT` | `4242` | Listen port | | `CANON_DATA_DIR` | `/data` (Docker), `./data` | Database + cached images | | `CANON_ADMIN_USERNAME` / `_PASSWORD` | | Create the owner account on first start | diff --git a/cmd/canon/main.go b/cmd/canon/main.go index cdf6278..3c56dcf 100644 --- a/cmd/canon/main.go +++ b/cmd/canon/main.go @@ -9,6 +9,7 @@ import ( "fmt" "log/slog" "math/big" + "net" "net/http" "os" "os/signal" @@ -93,6 +94,7 @@ func run(log *slog.Logger) error { errc := make(chan error, 1) go func() { log.Info("Canon is running", "addr", cfg.ListenAddr, "data", cfg.DataDir, "version", version) + logURLs(log, cfg) errc <- httpSrv.ListenAndServe() }() select { @@ -149,3 +151,49 @@ func setupCode() string { } return string(b[:4]) + "-" + string(b[4:]) } + +// logURLs prints the addresses other devices can use to reach Canon. +func logURLs(log *slog.Logger, cfg config.Config) { + if cfg.Host != "0.0.0.0" && cfg.Host != "" && cfg.Host != "::" { + log.Info("open Canon at", "url", "http://"+net.JoinHostPort(cfg.Host, cfg.Port)) + return + } + // Inside Docker the container's own addresses aren't reachable from the + // network; the host's address and published port are what matter. + if _, err := os.Stat("/.dockerenv"); err == nil { + log.Info("listening on all interfaces; open http://: from any device on your network") + return + } + log.Info("open Canon at", "url", "http://localhost:"+cfg.Port) + ifaces, err := net.Interfaces() + if err != nil { + return + } + for _, iface := range ifaces { + // Skip interfaces that are down and container/VM bridges, which + // other devices can't reach. + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 || isVirtualBridge(iface.Name) { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, a := range addrs { + ipnet, ok := a.(*net.IPNet) + if !ok || ipnet.IP.To4() == nil || ipnet.IP.IsLinkLocalUnicast() { + continue + } + log.Info("open Canon on your network at", "url", "http://"+net.JoinHostPort(ipnet.IP.String(), cfg.Port)) + } + } +} + +func isVirtualBridge(name string) bool { + for _, prefix := range []string{"docker", "br-", "veth", "virbr", "cni", "flannel", "podman"} { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} diff --git a/docker-compose.yml b/docker-compose.yml index d1ab838..4669b95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,9 @@ services: container_name: canon restart: unless-stopped ports: - - "4242:4242" + # 0.0.0.0 makes Canon reachable from other devices on your network. + # Use "127.0.0.1:4242:4242" to keep it on this machine only. + - "0.0.0.0:4242:4242" volumes: - canon-data:/data env_file: diff --git a/internal/config/config.go b/internal/config/config.go index 1f951b9..dc2774c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( + "net" "os" "strconv" "strings" @@ -12,6 +13,8 @@ import ( // fields here only act as overrides when set. type Config struct { ListenAddr string + Host string + Port string DataDir string AdminUsername string AdminPassword string @@ -28,9 +31,14 @@ type Config struct { } func Load() Config { + // 0.0.0.0 accepts connections from other devices on the network, not + // just this machine. Use 127.0.0.1 to keep it local. + host := env("CANON_HOST", "0.0.0.0") port := env("CANON_PORT", "4242") return Config{ - ListenAddr: env("CANON_LISTEN", ":"+port), + ListenAddr: env("CANON_LISTEN", net.JoinHostPort(host, port)), + Host: host, + Port: port, DataDir: env("CANON_DATA_DIR", "./data"), AdminUsername: env("CANON_ADMIN_USERNAME", ""), AdminPassword: env("CANON_ADMIN_PASSWORD", ""),