From 356e98e2bf323d7ea697f6d7cb85f07a9565c246 Mon Sep 17 00:00:00 2001 From: oidq Date: Mon, 10 Nov 2025 14:13:05 +0100 Subject: [PATCH] fix: Add IPv6 handling to handlers.FailSaveAddress() On IPv6-only deployment I encountered two problem with collaboration service and CheckHandler: * getOutBoundIP() did not allow IPv6 addresses to be used as outbound * FailSaveAddress() did double interpolation of the "::" literal on GUA IPv6 addresses containing "::" --- ocis-pkg/checks/checkhttp.go | 16 +++++--- ocis-pkg/handlers/checker.go | 63 +++++++++++++++++++++++++------ ocis-pkg/handlers/checker_test.go | 18 +++++++++ 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/ocis-pkg/checks/checkhttp.go b/ocis-pkg/checks/checkhttp.go index 48502fe6ac6..e74c9e1bb6c 100644 --- a/ocis-pkg/checks/checkhttp.go +++ b/ocis-pkg/checks/checkhttp.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "strings" "time" @@ -11,21 +12,26 @@ import ( ) // NewHTTPCheck checks the reachability of a http server. -func NewHTTPCheck(url string) func(context.Context) error { +func NewHTTPCheck(rawUrl string) func(context.Context) error { return func(_ context.Context) error { - url, err := handlers.FailSaveAddress(url) + if !strings.HasPrefix(rawUrl, "http://") && !strings.HasPrefix(rawUrl, "https://") { + rawUrl = "http://" + rawUrl + } + + parsedUrl, err := url.Parse(rawUrl) if err != nil { return err } - if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { - url = "http://" + url + parsedUrl.Host, err = handlers.FailSaveAddress(parsedUrl.Host) + if err != nil { + return err } c := http.Client{ Timeout: 3 * time.Second, } - resp, err := c.Get(url) + resp, err := c.Get(parsedUrl.String()) if err != nil { return fmt.Errorf("could not connect to http server: %v", err) } diff --git a/ocis-pkg/handlers/checker.go b/ocis-pkg/handlers/checker.go index 0326c580dcb..8ef40ebec59 100644 --- a/ocis-pkg/handlers/checker.go +++ b/ocis-pkg/handlers/checker.go @@ -115,34 +115,75 @@ func (h *CheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -// FailSaveAddress replaces wildcard addresses with the outbound IP. +// FailSaveAddress replaces unspecified addresses with the outbound IP. func FailSaveAddress(address string) (string, error) { - if strings.Contains(address, "0.0.0.0") || strings.Contains(address, "::") { - outboundIp, err := getOutBoundIP() + host, port := SplitHostPort(address) + + hostIP := net.ParseIP(host) + + if host == "" || (hostIP != nil && hostIP.IsUnspecified()) { + outboundIP, err := getOutBoundIP() if err != nil { return "", err } - address = strings.Replace(address, "0.0.0.0", outboundIp, 1) - address = strings.Replace(address, "::", "["+outboundIp+"]", 1) - address = strings.Replace(address, "[::]", "["+outboundIp+"]", 1) + + host = outboundIP.String() } - return address, nil + + if port != "" { + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + return host + ":" + port, nil + } + + return host, nil +} + +// SplitHostPort returns host and port of the address. +// Contrary to the net.SplitHostPort the port is not mandatory. +func SplitHostPort(address string) (string, string) { + columns := strings.Split(address, ":") + brackets := strings.Split(address, "]") + + switch { + case len(columns) == 1 && len(brackets) == 1: // 10.10.10.10 + return address, "" + case len(columns) == 2 && len(brackets) == 1: // 10.10.10.10:80 + return columns[0], columns[1] + case len(columns) > 2 && len(brackets) == 1: // 2a01::a + return address, "" + case len(brackets) == 2 && brackets[1] == "": // [2a01::a] + return brackets[0][1:], "" + case len(brackets) == 2: // [2a01::a]:10 + return brackets[0][1:], columns[len(columns)-1] + } + + return address, "" } // getOutBoundIP returns the outbound IP address. -func getOutBoundIP() (string, error) { +func getOutBoundIP() (net.IP, error) { interfacesAddresses, err := net.InterfaceAddrs() if err != nil { - return "", err + return nil, err } + var fallbackIpv6 net.IP for _, address := range interfacesAddresses { if ipNet, ok := address.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { if ipNet.IP.To4() != nil { - return ipNet.IP.String(), nil + return ipNet.IP, nil + } + if ipNet.IP.To16() != nil && !ipNet.IP.IsLinkLocalUnicast() { + fallbackIpv6 = ipNet.IP.To16() } } } - return "", fmt.Errorf("no IP found") + if fallbackIpv6 != nil { + return fallbackIpv6, nil + } + + return nil, fmt.Errorf("no IP found") } diff --git a/ocis-pkg/handlers/checker_test.go b/ocis-pkg/handlers/checker_test.go index 3a87111b6f2..25e9ec468a6 100644 --- a/ocis-pkg/handlers/checker_test.go +++ b/ocis-pkg/handlers/checker_test.go @@ -125,3 +125,21 @@ func TestCheckHandler(t *testing.T) { require.Equal(t, 2, len(slices.DeleteFunc(errs, func(err error) bool { return err != nil }))) }) } + +func TestSplitHostPort(t *testing.T) { + address, port := handlers.SplitHostPort("10.10.10.10") + require.Equal(t, address, "10.10.10.10") + require.Equal(t, port, "") + + address, port = handlers.SplitHostPort("10.10.10.10:20") + require.Equal(t, address, "10.10.10.10") + require.Equal(t, port, "20") + + address, port = handlers.SplitHostPort("2a01::1") + require.Equal(t, address, "2a01::1") + require.Equal(t, port, "") + + address, port = handlers.SplitHostPort("[2a01::1]:10") + require.Equal(t, address, "2a01::1") + require.Equal(t, port, "10") +}