gopls/internal/cmd: remove gopls -port=int debugging flag

The -port flag is confusingly redundant with -listen=address,
as the existing TODO notes, and it may bind INADDR_ANY.

Also, change these flags to reject implicit use of INADDR_ANY (e.g. ":12345")
and suggest using an explicit host of either 0.0.0.0 or localhost (recommended):
 gopls serve -listen=address
 gopls serve -mcp.listen=address
 gopls mcp -listen=address

+ relnote

Fixes golang/go#79211
Fixes CVE-2026-42503

Change-Id: Ia42202d3239468480bd231eeb0ffea88b238ddc6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/774381
Reviewed-by: Nicholas Husin <nsh@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
diff --git a/gopls/doc/release/v0.22.0.md b/gopls/doc/release/v0.22.0.md
index bebb067..ae20a84 100644
--- a/gopls/doc/release/v0.22.0.md
+++ b/gopls/doc/release/v0.22.0.md
@@ -58,6 +58,9 @@
 - The experimental `renameMovesSubpackages` setting determines whether
   a package-rename operation should also move subdirectories.
 - The `importsSource` setting is deprecated and will be removed in the next release.
+- The `-port=int` debugging flag (redundant with `-listen`) has been removed.
+- The `-listen=address` flag now rejects an implicit host (e.g. `:0`).
+  You must explicitly specify a host such as `0.0.0.0` or (preferably) `localhost`.
 
 ## Navigation features
 
diff --git a/gopls/internal/cmd/serve.go b/gopls/internal/cmd/serve.go
index 0be5aaa..a56fc89 100644
--- a/gopls/internal/cmd/serve.go
+++ b/gopls/internal/cmd/serve.go
@@ -12,6 +12,7 @@
 	"io"
 	"log"
 	"os"
+	"strings"
 	"time"
 
 	"golang.org/x/sync/errgroup"
@@ -30,7 +31,6 @@
 type Serve struct {
 	Logfile     string        `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"`
 	Mode        string        `flag:"mode" help:"no effect"`
-	Port        int           `flag:"port" help:"port on which to run gopls for debugging purposes"`
 	Address     string        `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."`
 	IdleTimeout time.Duration `flag:"listen.timeout" help:"when used with -listen, shut down the server when there are no connected clients for this duration"`
 	Trace       bool          `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"`
@@ -87,7 +87,7 @@
 	}
 
 	di := debug.GetInstance(ctx)
-	isDaemon := s.Address != "" || s.Port != 0
+	isDaemon := s.Address != ""
 	if di != nil {
 		closeLog, err := di.SetLogFile(s.Logfile, isDaemon)
 		if err != nil {
@@ -143,37 +143,17 @@
 			}
 		}()
 
-		var network, addr string
 		if s.Address != "" {
-			network, addr = lsprpc.ParseAddr(s.Address)
-		}
-		if s.Port != 0 {
-			network = "tcp"
-			// TODO(adonovan): should gopls ever be listening on network
-			// sockets, or only local ones?
-			//
-			// Ian says this was added in anticipation of
-			// something related to "VS Code remote" that turned
-			// out to be unnecessary. So I propose we limit it to
-			// localhost, if only so that we avoid the macOS
-			// firewall prompt.
-			//
-			// Hana says: "s.Address is for the remote access (LSP)
-			// and s.Port is for debugging purpose (according to
-			// the Server type documentation). I am not sure why the
-			// existing code here is mixing up and overwriting addr.
-			// For debugging endpoint, I think localhost makes perfect sense."
-			//
-			// TODO(adonovan): disentangle Address and Port,
-			// and use only localhost for the latter.
-			addr = fmt.Sprintf(":%v", s.Port)
-		}
-
-		if addr != "" {
+			// -listen=address
+			network, addr := lsprpc.ParseAddr(s.Address)
+			if strings.HasPrefix(addr, ":") {
+				return fmt.Errorf("-listen=%s implicitly binds all network interfaces; please use an explicit host such as 0.0.0.0 (all interfaces) or localhost (safer)", addr)
+			}
 			log.Printf("Gopls LSP daemon: listening on %s network, address %s...", network, addr)
 			defer log.Printf("Gopls LSP daemon: exiting")
 			return jsonrpc2.ListenAndServe(ctx, network, addr, ss, s.IdleTimeout)
 		} else {
+			// communicate over stdin/stdout
 			stream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", os.Stdin, os.Stdout))
 			if s.Trace && di != nil {
 				stream = protocol.LoggingStream(stream, di.LogWriter)
diff --git a/gopls/internal/cmd/usage/serve.hlp b/gopls/internal/cmd/usage/serve.hlp
index c72852d..4f2dc9b 100644
--- a/gopls/internal/cmd/usage/serve.hlp
+++ b/gopls/internal/cmd/usage/serve.hlp
@@ -20,8 +20,6 @@
     	experimental: address on which to listen for model context protocol connections. If port is localhost:0, pick a random port in localhost instead.
   -mode=string
     	no effect
-  -port=int
-    	port on which to run gopls for debugging purposes
   -remote.debug=string
     	when used with -remote=auto, the -debug value used to start the daemon
   -remote.listen.timeout=duration
diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp
index f9085ff..a1e497c 100644
--- a/gopls/internal/cmd/usage/usage-v.hlp
+++ b/gopls/internal/cmd/usage/usage-v.hlp
@@ -66,8 +66,6 @@
     	no effect
   -otel=string
     	export telemetry to specified OpenTelemetry collector address (e.g. http://localhost:4318)
-  -port=int
-    	port on which to run gopls for debugging purposes
   -profile.alloc=string
     	write alloc profile to this file
   -profile.block=string
diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp
index 465a25e..a3dbbf2 100644
--- a/gopls/internal/cmd/usage/usage.hlp
+++ b/gopls/internal/cmd/usage/usage.hlp
@@ -63,8 +63,6 @@
     	no effect
   -otel=string
     	export telemetry to specified OpenTelemetry collector address (e.g. http://localhost:4318)
-  -port=int
-    	port on which to run gopls for debugging purposes
   -profile.alloc=string
     	write alloc profile to this file
   -profile.block=string
diff --git a/gopls/internal/lsprpc/lsprpc.go b/gopls/internal/lsprpc/lsprpc.go
index 32c466e..c3228c5 100644
--- a/gopls/internal/lsprpc/lsprpc.go
+++ b/gopls/internal/lsprpc/lsprpc.go
@@ -583,7 +583,7 @@
 	}
 }
 
-// ParseAddr parses the address of a gopls remote.
+// ParseAddr parses the address of a gopls remote (e.g. "tcp;localhost:12345").
 // TODO(rFindley): further document this syntax, and allow URI-style remote
 // addresses such as "auto://...".
 func ParseAddr(listen string) (network string, address string) {
@@ -592,8 +592,8 @@
 	if listen == autoNetwork {
 		return autoNetwork, ""
 	}
-	if parts := strings.SplitN(listen, ";", 2); len(parts) == 2 {
-		return parts[0], parts[1]
+	if network, address, ok := strings.Cut(listen, ";"); ok {
+		return network, address
 	}
 	return "tcp", listen
 }
diff --git a/gopls/internal/mcp/mcp.go b/gopls/internal/mcp/mcp.go
index baa5d37..988e206 100644
--- a/gopls/internal/mcp/mcp.go
+++ b/gopls/internal/mcp/mcp.go
@@ -13,6 +13,7 @@
 	"net"
 	"net/http"
 	"os"
+	"strings"
 	"sync"
 
 	"github.com/google/jsonschema-go/jsonschema"
@@ -52,6 +53,9 @@
 // It is passed the list roots result returned by the MCP client, or an error
 // if the roots could not be retrieved. rootsHandler may be called concurrently.
 func Serve(ctx context.Context, address string, sessions Sessions, isDaemon bool, rootsHandler func(*mcp.ListRootsResult, error)) error {
+	if strings.HasPrefix(address, ":") {
+		return fmt.Errorf("address %s implicitly binds all network interfaces; please use an explicit host such as 0.0.0.0 (all interfaces) or localhost (safer)", address)
+	}
 	log.Printf("Gopls MCP server: starting up on http")
 	listener, err := net.Listen("tcp", address)
 	if err != nil {