From 8e9c8ca4e68fe6554b24c92bbf3fbf44f1f3d1d5 Mon Sep 17 00:00:00 2001 From: Minseong Choi Date: Tue, 22 Sep 2026 13:30:23 +0900 Subject: [PATCH] fix(nano): say that [server] listen is ignored instead of defaulting it LoadNano filled in [server] listen = "0.0.0.0:8080" when it was unset, and a test pinned that value, but felis nano never reads it: it binds the -listen flag, which the installer sets from FELIS_NANO_LISTEN. An operator moving nano off loopback by writing [server] listen in its config got connection refused from the proxy and no hint that the key did nothing. LoadNano no longer sets the default, and nano prints a line naming the ignored value and the address it actually binds whenever the key is set. It is a warning rather than a load error so a full felis.toml copied onto a nano host keeps starting. The assertion that pinned the unused default is removed along with it. The new test runs cmdNano against a config that sets [server] listen and one that does not, with an unbindable -listen so it returns after loading. The first must warn and the second must not; with the old default restored, the second prints a warning about 0.0.0.0:8080. --- cmd/felis/nano.go | 5 +++++ cmd/felis/nano_test.go | 28 ++++++++++++++++++++++++++++ internal/config/config.go | 3 --- internal/config/config_test.go | 5 +---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/cmd/felis/nano.go b/cmd/felis/nano.go index b994951..4a96618 100644 --- a/cmd/felis/nano.go +++ b/cmd/felis/nano.go @@ -70,6 +70,11 @@ func cmdNano(args []string, stdout, stderr io.Writer) int { fmt.Fprintln(stderr, "felis nano:", err) return 1 } + // [server] listen belongs to felis api. Someone moving nano off loopback naturally reaches + // for it, and without this line would get connection refused with no hint why. + if cfg.Server.Listen != "" { + fmt.Fprintf(stderr, "felis nano: [server] listen = %q is ignored; nano binds -listen (%s), which the installer sets from FELIS_NANO_LISTEN\n", cfg.Server.Listen, *listen) + } fmt.Fprintf(stderr, "felis nano: hasJoined multiplexer on %s — Mojang + %d third-party source(s)\n", *listen, len(cfg.AuthSources)) for i, s := range cfg.AuthSources { diff --git a/cmd/felis/nano_test.go b/cmd/felis/nano_test.go index 6ce40e5..87c8c62 100644 --- a/cmd/felis/nano_test.go +++ b/cmd/felis/nano_test.go @@ -7,6 +7,8 @@ import ( "net" "net/http" "net/http/httptest" + "os" + "path/filepath" "strconv" "strings" "testing" @@ -14,6 +16,32 @@ import ( "unicode/utf8" ) +// [server] listen in a nano config reads like the bind address but is not one; nano must +// say so. The -listen value cannot be bound, so cmdNano returns right after loading. +func TestNanoWarnsThatServerListenIsIgnored(t *testing.T) { + cfg := filepath.Join(t.TempDir(), "felis.toml") + if err := os.WriteFile(cfg, []byte("[server]\nlisten = \"0.0.0.0:9999\"\n"), 0o600); err != nil { + t.Fatal(err) + } + var stderr bytes.Buffer + if rc := cmdNano([]string{"-config", cfg, "-listen", "127.0.0.1:-1"}, io.Discard, &stderr); rc != 1 { + t.Fatalf("cmdNano = %d, want 1 from the unbindable -listen", rc) + } + if !strings.Contains(stderr.String(), `listen = "0.0.0.0:9999" is ignored`) { + t.Fatalf("stderr %q should say the configured listen is ignored", stderr.String()) + } + + // With no [server] table at all there is nothing to warn about. + if err := os.WriteFile(cfg, nil, 0o600); err != nil { + t.Fatal(err) + } + stderr.Reset() + _ = cmdNano([]string{"-config", cfg, "-listen", "127.0.0.1:-1"}, io.Discard, &stderr) + if strings.Contains(stderr.String(), "is ignored") { + t.Fatalf("stderr %q warns about a listen the operator never set", stderr.String()) + } +} + // A stop signal that lands while a login is waiting on an upstream must let that login // finish: the request is answered, and serveNano returns only afterwards. func TestNanoDrainsInFlightLoginOnShutdown(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 949a7b7..cb6056f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -257,9 +257,6 @@ func LoadNano(path string) (*Config, error) { if err != nil { return nil, err } - if cfg.Server.Listen == "" { - cfg.Server.Listen = defaultListen - } if err := cfg.validateAuthSources(); err != nil { return nil, err } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b2b9024..fa74c9e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -379,7 +379,7 @@ func TestLoadRejectsUnqueryableAuthSourceURL(t *testing.T) { // TestLoadNanoAcceptsMinimalConfig is the linchpin of the Felis-nano fold: a nano host has no // Postgres and no FQDN, so LoadNano must accept a felis.toml carrying ONLY [[auth_source]] — // the control-plane requirements (database.url, root_domain) that full Load enforces are -// deliberately skipped. It still applies the listen default and hands back the sources. +// deliberately skipped. It hands back the sources. func TestLoadNanoAcceptsMinimalConfig(t *testing.T) { cfg, err := config.LoadNano(writeTOML(t, ` [[auth_source]] @@ -393,9 +393,6 @@ url = "https://littleskin.example.net/api/yggdrasil/sessionserver/session/minecr if len(cfg.AuthSources) != 1 || cfg.AuthSources[0].Tag != "littleskin" { t.Fatalf("auth sources = %+v, want one littleskin source", cfg.AuthSources) } - if cfg.Server.Listen != "0.0.0.0:8080" { - t.Errorf("default listen = %q, want 0.0.0.0:8080", cfg.Server.Listen) - } } // TestLoadNanoStillEnforcesAuthSourceRules pins that skipping the control-plane requirements