Unverified Commit 8e9c8ca4 authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

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.
parent 1d6c7300
Loading
Loading
Loading
Loading
+5 −0
Changes for cmd/felis/nano.go: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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 {
+28 −0
Changes for cmd/felis/nano_test.go: 28 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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) {
+0 −3
Changes for internal/config/config.go: 0 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -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
	}
+1 −4
Changes for internal/config/config_test.go: 1 added line, 4 removed lines.
Original line number Diff line number Diff line
@@ -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