From 6628c4a9de5588e43430b285f6f4de376aaafe70 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 21 May 2026 00:17:34 +1000 Subject: [PATCH] cmd: support caddy start on IPv6-only hosts (#7744) --- cmd/commandfuncs.go | 18 ++++++++++- cmd/main_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index faa275b03..56cde4758 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -58,7 +58,7 @@ func cmdStart(fl Flags) (int, error) { // open a listener to which the child process will connect when // it is ready to confirm that it has successfully started - ln, err := net.Listen("tcp", "127.0.0.1:0") + ln, err := listenTCPForPingback(net.Listen) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("opening listener for success confirmation: %v", err) @@ -169,6 +169,22 @@ func cmdStart(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } +type tcpListenFunc func(network, address string) (net.Listener, error) + +func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) { + ln, ipv4Err := listen("tcp4", "127.0.0.1:0") + if ipv4Err == nil { + return ln, nil + } + + ln, ipv6Err := listen("tcp6", "[::1]:0") + if ipv6Err == nil { + return ln, nil + } + + return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err) +} + func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() diff --git a/cmd/main_test.go b/cmd/main_test.go index bff34f443..803574a9b 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -1,6 +1,8 @@ package caddycmd import ( + "errors" + "net" "reflect" "strings" "testing" @@ -169,6 +171,80 @@ here" } } +func TestListenTCPForPingbackUsesIPv4Loopback(t *testing.T) { + var calls []string + expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}} + + actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) { + calls = append(calls, network+" "+address) + return expected, nil + }) + if err != nil { + t.Fatalf("listenTCPForPingback returned error: %v", err) + } + if actual != expected { + t.Fatalf("expected listener %p, got %p", expected, actual) + } + + expectCalls := []string{"tcp4 127.0.0.1:0"} + if !reflect.DeepEqual(calls, expectCalls) { + t.Fatalf("expected calls %v, got %v", expectCalls, calls) + } +} + +func TestListenTCPForPingbackFallsBackToIPv6Loopback(t *testing.T) { + var calls []string + expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 1234}} + + actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) { + calls = append(calls, network+" "+address) + if len(calls) == 1 { + return nil, errors.New("ipv4 unavailable") + } + return expected, nil + }) + if err != nil { + t.Fatalf("listenTCPForPingback returned error: %v", err) + } + if actual != expected { + t.Fatalf("expected listener %p, got %p", expected, actual) + } + + expectCalls := []string{"tcp4 127.0.0.1:0", "tcp6 [::1]:0"} + if !reflect.DeepEqual(calls, expectCalls) { + t.Fatalf("expected calls %v, got %v", expectCalls, calls) + } +} + +func TestListenTCPForPingbackReportsBothFailures(t *testing.T) { + _, err := listenTCPForPingback(func(network, address string) (net.Listener, error) { + return nil, errors.New(network + " failed") + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "tcp4 failed") || + !strings.Contains(err.Error(), "tcp6 failed") { + t.Fatalf("expected both listener errors, got: %v", err) + } +} + +type stubListener struct { + addr net.Addr +} + +func (sl *stubListener) Accept() (net.Conn, error) { + return nil, net.ErrClosed +} + +func (sl *stubListener) Close() error { + return nil +} + +func (sl *stubListener) Addr() net.Addr { + return sl.addr +} + func Test_isCaddyfile(t *testing.T) { type args struct { configFile string