From 80cc2fe791f942dad5fd35b67364798d2be6c0e2 Mon Sep 17 00:00:00 2001 From: Arnob kumar saha Date: Wed, 12 Aug 2026 19:04:58 +0600 Subject: [PATCH] Fix NATS connection for IP-addressed and path-based event receivers The websocket handshake builds its request from the host plus Options.ProxyPath and drops the path of the server URL, so a receiver address like wss://10.2.1.39:443/nats requested / and failed with "invalid websocket connection". Pass the path as nats.ProxyPath. Also default a missing port to 443, and skip TLS hostname verification when the receiver is addressed by a bare IP, which can never match the serving cert SANs. Signed-off-by: Arnob kumar saha --- lib/nats.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/nats.go b/lib/nats.go index 55963044..e34ef3dd 100644 --- a/lib/nats.go +++ b/lib/nats.go @@ -18,9 +18,12 @@ package lib import ( "context" + "crypto/tls" "errors" "fmt" + "net" "net/http" + "net/url" "os" "strings" "sync" @@ -255,11 +258,49 @@ func isNoConnectivityErr(_ error) bool { return false } +// normalizeServers defaults the port of each server in the comma separated +// list to 443 when none is given, reports whether any of them is addressed by +// a bare IP instead of a hostname, and returns the URL path, if any. The +// websocket handshake builds its request from the host plus Options.ProxyPath +// and drops the path of the server URL, so the path has to be handed back +// separately and passed as nats.ProxyPath. +func normalizeServers(servers string) (normalized string, isIP bool, proxyPath string) { + var out []string + for s := range strings.SplitSeq(servers, ",") { + s = strings.TrimSpace(s) + if s == "" { + continue + } + toParse := s + if !strings.Contains(toParse, "://") { + toParse = "nats://" + toParse + } + u, err := url.Parse(toParse) + if err != nil { + klog.V(5).InfoS("failed to parse event receiver address", "address", s, "error", err) + out = append(out, s) + continue + } + if net.ParseIP(u.Hostname()) != nil { + isIP = true + } + if path := strings.TrimSuffix(u.Path, "/"); path != "" && proxyPath == "" { + proxyPath = path + } + if u.Port() == "" { + u.Host = net.JoinHostPort(u.Hostname(), "443") + } + u.Path, u.RawQuery, u.Fragment = "", "", "" + out = append(out, u.String()) + } + return strings.Join(out, ","), isIP, proxyPath +} + // NewConnection creates a new NATS connection func NewConnection(licenseID string, natscred NatsCredential) (nc *nats.Conn, err error) { - servers := natscred.Server + servers, ipHost, proxyPath := normalizeServers(natscred.Server) - opts := make([]nats.Option, 0, 6) + opts := make([]nats.Option, 0, 8) opts = append( opts, nats.Name(fmt.Sprintf("%s.%s", licenseID, info.ProductName)), @@ -270,6 +311,23 @@ func NewConnection(licenseID string, natscred NatsCredential) (nc *nats.Conn, er // nats.UseOldRequestStyle(), ) + if proxyPath != "" { + opts = append(opts, nats.ProxyPath(proxyPath)) + } + + // A bare IP address can't be matched against the serving cert's SANs, so + // hostname verification would always fail. Only the TLSConfig is set here, + // not nats.Secure(), so a non-TLS server still connects in plain mode. + if ipHost { + opts = append(opts, func(o *nats.Options) error { + if o.TLSConfig == nil { + o.TLSConfig = &tls.Config{} // nolint:gosec + } + o.TLSConfig.InsecureSkipVerify = true // nolint:gosec + return nil + }) + } + credFile := "/tmp/nats.creds" if err = os.WriteFile(credFile, natscred.Credential, 0o600); err != nil { return nil, err @@ -290,6 +348,7 @@ func NewConnection(licenseID string, natscred NatsCredential) (nc *nats.Conn, er defer cancel() ticker := time.NewTicker(natsConnectionRetryInterval) + defer ticker.Stop() for { select { case <-ticker.C: