Repository: grpc/grpc-go

Issue:
NewClient doesn't handle link-local IPv6 addresses
### What version of gRPC are you using?
v1.68.0

### What version of Go are you using (`go version`)?
go version go1.23.2 darwin/amd64

### What operating system (Linux, Windows, …) and version?
Darwin MacBookPro 24.1.0 Darwin Kernel Version 24.1.0: Thu Oct 10 21:02:27 PDT 2024; root:xnu-11215.41.3~2/RELEASE_X86_64 x86_64 i386 Darwin

### What did you do?

I use `grpc.NewClient()` to connect using an address without an IP e.g. ":1234", and then make a call to a gRPC method.

### What did you expect to see?
A successful call (it works on Linux)

### What did you see instead?

I get an error saying: "dns: error parsing A record IP address fe80::1%lo0"

The problem is that the [dnsResolver.lookupHost](https://github.com/grpc/grpc-go/blob/master/internal/resolver/dns/dns_resolver.go#L317) assumes that all IPs found for "localhost" in `/etc/hosts` can be parsed by `net.ParseIP` (in `formatIP`). On many macOS systems this is not the case, because they contain an entry similar to `fe80::1%lo0  localhost`.


Hints from issue comments:
@thallgren: The `net.ParseIP` will delegate to `netip.ParseAddr` and then fail if the parsed address contains a zone.@arjan-bal: Hi @thallgren, thanks for reporting the issue. It looks like the DNS resolver (which is the default when using grpc.NewClient) doesn't handle link-local IPv6 addresses correctly due to the presence of the `interface`. The format of link-local IPv6 addresses is `[IPv6-address%interface]:port`. `net.Dial` is able to connect to such addresses, so using the deprecated `grpc.Dial()` should work.

Let me discuss the scope of the fix with other maintainers and get back.

Solution patch:
diff --git a/balancer/pickfirst/pickfirstleaf/pickfirstleaf.go b/balancer/pickfirst/pickfirstleaf/pickfirstleaf.go
index 1ebf7cea..7a250e6e 100644
--- a/balancer/pickfirst/pickfirstleaf/pickfirstleaf.go
+++ b/balancer/pickfirst/pickfirstleaf/pickfirstleaf.go
@@ -30,6 +30,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"net/netip"
 	"sync"
 	"time"
 
@@ -417,11 +418,14 @@ func addressFamily(address string) ipAddrFamily {
 	if err != nil {
 		return ipAddrFamilyUnknown
 	}
-	ip := net.ParseIP(host)
+	ip, err := netip.ParseAddr(host)
+	if err != nil {
+		return ipAddrFamilyUnknown
+	}
 	switch {
-	case ip.To4() != nil:
+	case ip.Is4() || ip.Is4In6():
 		return ipAddrFamilyV4
-	case ip.To16() != nil:
+	case ip.Is6():
 		return ipAddrFamilyV6
 	default:
 		return ipAddrFamilyUnknown
diff --git a/internal/resolver/dns/dns_resolver.go b/internal/resolver/dns/dns_resolver.go
index b080ae30..ba5c5a95 100644
--- a/internal/resolver/dns/dns_resolver.go
+++ b/internal/resolver/dns/dns_resolver.go
@@ -26,6 +26,7 @@ import (
 	"fmt"
 	rand "math/rand/v2"
 	"net"
+	"net/netip"
 	"os"
 	"strconv"
 	"strings"
@@ -122,7 +123,7 @@ func (b *dnsBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts
 	}
 
 	// IP address.
-	if ipAddr, ok := formatIP(host); ok {
+	if ipAddr, err := formatIP(host); err == nil {
 		addr := []resolver.Address{{Addr: ipAddr + ":" + port}}
 		cc.UpdateState(resolver.State{Addresses: addr})
 		return deadResolver{}, nil
@@ -260,9 +261,9 @@ func (d *dnsResolver) lookupSRV(ctx context.Context) ([]resolver.Address, error)
 			return nil, err
 		}
 		for _, a := range lbAddrs {
-			ip, ok := formatIP(a)
-			if !ok {
-				return nil, fmt.Errorf("dns: error parsing A record IP address %v", a)
+			ip, err := formatIP(a)
+			if err != nil {
+				return nil, fmt.Errorf("dns: error parsing A record IP address %v: %v", a, err)
 			}
 			addr := ip + ":" + strconv.Itoa(int(s.Port))
 			newAddrs = append(newAddrs, resolver.Address{Addr: addr, ServerName: s.Target})
@@ -322,9 +323,9 @@ func (d *dnsResolver) lookupHost(ctx context.Context) ([]resolver.Address, error
 	}
 	newAddrs := make([]resolver.Address, 0, len(addrs))
 	for _, a := range addrs {
-		ip, ok := formatIP(a)
-		if !ok {
-			return nil, fmt.Errorf("dns: error parsing A record IP address %v", a)
+		ip, err := formatIP(a)
+		if err != nil {
+			return nil, fmt.Errorf("dns: error parsing A record IP address %v: %v", a, err)
 		}
 		addr := ip + ":" + d.port
 		newAddrs = append(newAddrs, resolver.Address{Addr: addr})
@@ -351,19 +352,19 @@ func (d *dnsResolver) lookup() (*resolver.State, error) {
 	return &state, nil
 }
 
-// formatIP returns ok = false if addr is not a valid textual representation of
-// an IP address. If addr is an IPv4 address, return the addr and ok = true.
+// formatIP returns an error if addr is not a valid textual representation of
+// an IP address. If addr is an IPv4 address, return the addr and error = nil.
 // If addr is an IPv6 address, return the addr enclosed in square brackets and
-// ok = true.
-func formatIP(addr string) (addrIP string, ok bool) {
-	ip := net.ParseIP(addr)
-	if ip == nil {
-		return "", false
+// error = nil.
+func formatIP(addr string) (string, error) {
+	ip, err := netip.ParseAddr(addr)
+	if err != nil {
+		return "", err
 	}
-	if ip.To4() != nil {
-		return addr, true
+	if ip.Is4() {
+		return addr, nil
 	}
-	return "[" + addr + "]", true
+	return "[" + addr + "]", nil
 }
 
 // parseTarget takes the user input target string and default port, returns
@@ -379,7 +380,7 @@ func parseTarget(target, defaultPort string) (host, port string, err error) {
 	if target == "" {
 		return "", "", internal.ErrMissingAddr
 	}
-	if ip := net.ParseIP(target); ip != nil {
+	if _, err := netip.ParseAddr(target); err == nil {
 		// target is an IPv4 or IPv6(without brackets) address
 		return target, defaultPort, nil
 	}
diff --git a/internal/xds/rbac/matchers.go b/internal/xds/rbac/matchers.go
index e1c15018..07bb59ce 100644
--- a/internal/xds/rbac/matchers.go
+++ b/internal/xds/rbac/matchers.go
@@ -20,6 +20,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"net/netip"
 	"regexp"
 
 	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
@@ -344,7 +345,8 @@ func newRemoteIPMatcher(cidrRange *v3corepb.CidrRange) (*remoteIPMatcher, error)
 }
 
 func (sim *remoteIPMatcher) match(data *rpcData) bool {
-	return sim.ipNet.Contains(net.IP(net.ParseIP(data.peerInfo.Addr.String())))
+	ip, _ := netip.ParseAddr(data.peerInfo.Addr.String())
+	return sim.ipNet.Contains(net.IP(ip.AsSlice()))
 }
 
 type localIPMatcher struct {
@@ -361,7 +363,8 @@ func newLocalIPMatcher(cidrRange *v3corepb.CidrRange) (*localIPMatcher, error) {
 }
 
 func (dim *localIPMatcher) match(data *rpcData) bool {
-	return dim.ipNet.Contains(net.IP(net.ParseIP(data.localAddr.String())))
+	ip, _ := netip.ParseAddr(data.localAddr.String())
+	return dim.ipNet.Contains(net.IP(ip.AsSlice()))
 }
 
 // portMatcher matches on whether the destination port of the RPC matches the
diff --git a/scripts/common.sh b/scripts/common.sh
index dede0789..3e641bae 100755
--- a/scripts/common.sh
+++ b/scripts/common.sh
@@ -11,6 +11,13 @@ not() {
   ! "$@"
 }
 
+# noret_grep will return 0 if zero or more lines were selected, and >1 if an
+# error occurred. Suppresses grep's return code of 1 when there are no matches
+# (for eg, empty file).
+noret_grep() {
+  grep "$@" || [[ $? == 1 ]]
+}
+
 die() {
   echo "$@" >&2
   exit 1
diff --git a/scripts/gen-deps.sh b/scripts/gen-deps.sh
index bc647c4d..f4d5ec63 100755
--- a/scripts/gen-deps.sh
+++ b/scripts/gen-deps.sh
@@ -3,6 +3,8 @@
 set -e          # Exit on error
 set -o pipefail # Fail a pipe if any sub-command fails.
 
+source "$(dirname $0)/common.sh"
+
 if [[ "$#" -ne 1 || ! -d "$1" ]]; then
     echo "Specify a valid output directory as the first parameter."
     exit 1
@@ -16,6 +18,6 @@ cd "${SCRIPTS_DIR}/.."
 git ls-files -- '*.go' | grep -v '\(^\|/\)\(internal\|examples\|benchmark\|interop\|test\|testdata\)\(/\|$\)' | xargs dirname | sort -u | while read d; do
   pushd "$d" > /dev/null
   pkg="$(echo "$d" | sed 's;\.;grpc;' | sed 's;/;_;g')"
-  go list -deps . | sort >| "${OUTPUT_DIR}/$pkg"
+  go list -deps . | sort | noret_grep -v 'google.golang.org/grpc' >| "${OUTPUT_DIR}/$pkg"
   popd > /dev/null
 done
diff --git a/scripts/vet.sh b/scripts/vet.sh
index 8db7e19c..4867e8c6 100755
--- a/scripts/vet.sh
+++ b/scripts/vet.sh
@@ -8,13 +8,6 @@ source "$(dirname $0)/common.sh"
 # Check to make sure it's safe to modify the user's git repo.
 git status --porcelain | fail_on_output
 
-# noret_grep will return 0 if zero or more lines were selected, and >1 if an
-# error occurred. Suppresses grep's return code of 1 when there are no matches
-# (for eg, empty file).
-noret_grep() {
-  grep "$@" || [[ $? == 1 ]]
-}
-
 # Undo any edits made by this script.
 cleanup() {
   git reset --hard HEAD
@@ -89,6 +82,10 @@ git grep '"github.com/envoyproxy/go-control-plane/envoy' -- '*.go' ':(exclude)*.
 git grep -e 'context.Background()' --or -e 'context.TODO()' -- "*_test.go" | grep -v "benchmark/primitives/context_test.go" | grep -v "credential
 s/google" | grep -v "internal/transport/" | grep -v "xds/internal/" | grep -v "security/advancedtls" | grep -v 'context.WithTimeout(' | not grep -v 'context.WithCancel('
 
+# Disallow usage of net.ParseIP in favour of netip.ParseAddr as the former
+# can't parse link local IPv6 addresses.
+not git grep 'net.ParseIP' -- '*.go'
+
 misspell -error .
 
 # - gofmt, goimports, go vet, go mod tidy.



Test patch:
diff --git a/balancer/pickfirst/pickfirstleaf/pickfirstleaf_ext_test.go b/balancer/pickfirst/pickfirstleaf/pickfirstleaf_ext_test.go
index 00715724..69461625 100644
--- a/balancer/pickfirst/pickfirstleaf/pickfirstleaf_ext_test.go
+++ b/balancer/pickfirst/pickfirstleaf/pickfirstleaf_ext_test.go
@@ -1091,7 +1091,7 @@ func (s) TestPickFirstLeaf_InterleavingIPV4Preffered(t *testing.T) {
 				{Addresses: []resolver.Address{{Addr: "[::FFFF:192.168.0.1]:2222"}}},
 				{Addresses: []resolver.Address{{Addr: "[0001:0001:0001:0001:0001:0001:0001:0001]:8080"}}},
 				{Addresses: []resolver.Address{{Addr: "[0002:0002:0002:0002:0002:0002:0002:0002]:8080"}}},
-				{Addresses: []resolver.Address{{Addr: "[0003:0003:0003:0003:0003:0003:0003:0003]:3333"}}},
+				{Addresses: []resolver.Address{{Addr: "[fe80::1%eth0]:3333"}}},
 				{Addresses: []resolver.Address{{Addr: "grpc.io:80"}}}, // not an IP.
 			},
 		},
@@ -1107,7 +1107,7 @@ func (s) TestPickFirstLeaf_InterleavingIPV4Preffered(t *testing.T) {
 		{Addr: "2.2.2.2:2"},
 		{Addr: "[0002:0002:0002:0002:0002:0002:0002:0002]:8080"},
 		{Addr: "3.3.3.3:3"},
-		{Addr: "[0003:0003:0003:0003:0003:0003:0003:0003]:3333"},
+		{Addr: "[fe80::1%eth0]:3333"},
 		{Addr: "[::FFFF:192.168.0.1]:2222"},
 	}
 
@@ -1135,7 +1135,7 @@ func (s) TestPickFirstLeaf_InterleavingIPv6Preffered(t *testing.T) {
 				{Addresses: []resolver.Address{{Addr: "3.3.3.3:3"}}},
 				{Addresses: []resolver.Address{{Addr: "[::FFFF:192.168.0.1]:2222"}}},
 				{Addresses: []resolver.Address{{Addr: "[0002:0002:0002:0002:0002:0002:0002:0002]:2222"}}},
-				{Addresses: []resolver.Address{{Addr: "[0003:0003:0003:0003:0003:0003:0003:0003]:3333"}}},
+				{Addresses: []resolver.Address{{Addr: "[fe80::1%eth0]:3333"}}},
 				{Addresses: []resolver.Address{{Addr: "grpc.io:80"}}}, // not an IP.
 			},
 		},
@@ -1150,7 +1150,7 @@ func (s) TestPickFirstLeaf_InterleavingIPv6Preffered(t *testing.T) {
 		{Addr: "grpc.io:80"},
 		{Addr: "[0002:0002:0002:0002:0002:0002:0002:0002]:2222"},
 		{Addr: "2.2.2.2:2"},
-		{Addr: "[0003:0003:0003:0003:0003:0003:0003:0003]:3333"},
+		{Addr: "[fe80::1%eth0]:3333"},
 		{Addr: "3.3.3.3:3"},
 		{Addr: "[::FFFF:192.168.0.1]:2222"},
 	}
@@ -1180,7 +1180,7 @@ func (s) TestPickFirstLeaf_InterleavingUnknownPreffered(t *testing.T) {
 				{Addresses: []resolver.Address{{Addr: "[::FFFF:192.168.0.1]:2222"}}},
 				{Addresses: []resolver.Address{{Addr: "[0001:0001:0001:0001:0001:0001:0001:0001]:8080"}}},
 				{Addresses: []resolver.Address{{Addr: "[0002:0002:0002:0002:0002:0002:0002:0002]:8080"}}},
-				{Addresses: []resolver.Address{{Addr: "[0003:0003:0003:0003:0003:0003:0003:0003]:3333"}}},
+				{Addresses: []resolver.Address{{Addr: "[fe80::1%eth0]:3333"}}},
 				{Addresses: []resolver.Address{{Addr: "example.com:80"}}}, // not an IP.
 			},
 		},
@@ -1197,7 +1197,7 @@ func (s) TestPickFirstLeaf_InterleavingUnknownPreffered(t *testing.T) {
 		{Addr: "2.2.2.2:2"},
 		{Addr: "[0002:0002:0002:0002:0002:0002:0002:0002]:8080"},
 		{Addr: "3.3.3.3:3"},
-		{Addr: "[0003:0003:0003:0003:0003:0003:0003:0003]:3333"},
+		{Addr: "[fe80::1%eth0]:3333"},
 		{Addr: "[::FFFF:192.168.0.1]:2222"},
 	}
 
diff --git a/channelz/service/service_test.go b/channelz/service/service_test.go
index 78a5f2f8..111e5d17 100644
--- a/channelz/service/service_test.go
+++ b/channelz/service/service_test.go
@@ -22,6 +22,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"net/netip"
 	"strconv"
 	"strings"
 	"testing"
@@ -605,8 +606,8 @@ func (s) TestGetSocket(t *testing.T) {
 		lastMessageReceivedTimestamp:     time.Unix(3, 0),
 		localFlowControlWindow:           65536,
 		remoteFlowControlWindow:          1024,
-		localAddr:                        &net.TCPAddr{IP: net.ParseIP("1.0.0.1"), Port: 10001},
-		remoteAddr:                       &net.TCPAddr{IP: net.ParseIP("12.0.0.1"), Port: 10002},
+		localAddr:                        &net.TCPAddr{IP: netip.MustParseAddr("1.0.0.1").AsSlice(), Port: 10001},
+		remoteAddr:                       &net.TCPAddr{IP: netip.MustParseAddr("12.0.0.1").AsSlice(), Port: 10002},
 		remoteName:                       "remote.remote",
 	}), newSocket(czSocket{
 		streamsStarted:                   10,
@@ -637,11 +638,11 @@ func (s) TestGetSocket(t *testing.T) {
 		lastMessageReceivedTimestamp:     time.Unix(0, 0),
 		localFlowControlWindow:           65536,
 		remoteFlowControlWindow:          10240,
-		localAddr:                        &net.IPAddr{IP: net.ParseIP("1.0.0.1")},
-		remoteAddr:                       &net.IPAddr{IP: net.ParseIP("9.0.0.1")},
+		localAddr:                        &net.IPAddr{IP: netip.MustParseAddr("1.0.0.1").AsSlice()},
+		remoteAddr:                       &net.IPAddr{IP: netip.MustParseAddr("9.0.0.1").AsSlice()},
 		remoteName:                       "",
 	}), newSocket(czSocket{
-		localAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 10001},
+		localAddr: &net.TCPAddr{IP: netip.MustParseAddr("127.0.0.1").AsSlice(), Port: 10001},
 	}), newSocket(czSocket{
 		security: &credentials.TLSChannelzSecurityValue{
 			StandardName:      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
diff --git a/internal/credentials/xds/handshake_info_test.go b/internal/credentials/xds/handshake_info_test.go
index a6802405..a3e598be 100644
--- a/internal/credentials/xds/handshake_info_test.go
+++ b/internal/credentials/xds/handshake_info_test.go
@@ -21,6 +21,7 @@ package xds
 import (
 	"crypto/x509"
 	"net"
+	"net/netip"
 	"net/url"
 	"regexp"
 	"testing"
@@ -142,8 +143,11 @@ func TestMatchingSANExists_FailureCases(t *testing.T) {
 	inputCert := &x509.Certificate{
 		DNSNames:       []string{"foo.bar.example.com", "bar.baz.test.com", "*.example.com"},
 		EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"},
-		IPAddresses:    []net.IP{net.ParseIP("192.0.0.1"), net.ParseIP("2001:db8::68")},
-		URIs:           []*url.URL{url1, url2},
+		IPAddresses: []net.IP{
+			netip.MustParseAddr("192.0.0.1").AsSlice(),
+			netip.MustParseAddr("2001:db8::68").AsSlice(),
+		},
+		URIs: []*url.URL{url1, url2},
 	}
 
 	tests := []struct {
@@ -214,8 +218,11 @@ func TestMatchingSANExists_Success(t *testing.T) {
 	inputCert := &x509.Certificate{
 		DNSNames:       []string{"baz.test.com", "*.example.com"},
 		EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"},
-		IPAddresses:    []net.IP{net.ParseIP("192.0.0.1"), net.ParseIP("2001:db8::68")},
-		URIs:           []*url.URL{url1, url2},
+		IPAddresses: []net.IP{
+			netip.MustParseAddr("192.0.0.1").AsSlice(),
+			netip.MustParseAddr("2001:db8::68").AsSlice(),
+		},
+		URIs: []*url.URL{url1, url2},
 	}
 
 	tests := []struct {
diff --git a/internal/resolver/dns/dns_resolver_test.go b/internal/resolver/dns/dns_resolver_test.go
index 4e6a4d54..47285052 100644
--- a/internal/resolver/dns/dns_resolver_test.go
+++ b/internal/resolver/dns/dns_resolver_test.go
@@ -755,7 +755,16 @@ func (s) TestIPResolver(t *testing.T) {
 			target:   "[2001:db8::1]:http",
 			wantAddr: []resolver.Address{{Addr: "[2001:db8::1]:http"}},
 		},
-		// TODO(yuxuanli): zone support?
+		{
+			name:     "ipv6 with zone and port",
+			target:   "[fe80::1%25eth0]:1234",
+			wantAddr: []resolver.Address{{Addr: "[fe80::1%eth0]:1234"}},
+		},
+		{
+			name:     "ipv6 with zone and default port",
+			target:   "fe80::1%25eth0",
+			wantAddr: []resolver.Address{{Addr: "[fe80::1%eth0]:443"}},
+		},
 	}
 
 	for _, test := range tests {
diff --git a/internal/testutils/fakegrpclb/server.go b/internal/testutils/fakegrpclb/server.go
index 82be2c1a..b19e863f 100644
--- a/internal/testutils/fakegrpclb/server.go
+++ b/internal/testutils/fakegrpclb/server.go
@@ -24,6 +24,7 @@ import (
 	"fmt"
 	"io"
 	"net"
+	"net/netip"
 	"strconv"
 	"sync"
 	"time"
@@ -85,9 +86,9 @@ func NewServer(params ServerParams) (*Server, error) {
 		if err != nil {
 			return nil, fmt.Errorf("failed to parse list of backend address %q: %v", addr, err)
 		}
-		ip := net.ParseIP(ipStr)
-		if ip == nil {
-			return nil, fmt.Errorf("failed to parse ip: %q", ipStr)
+		ip, err := netip.ParseAddr(ipStr)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse ip %q: %v", ipStr, err)
 		}
 		port, err := strconv.Atoi(portStr)
 		if err != nil {
@@ -95,7 +96,7 @@ func NewServer(params ServerParams) (*Server, error) {
 		}
 		logger.Infof("Adding backend ip: %q, port: %d to server list", ip.String(), port)
 		servers = append(servers, &lbpb.Server{
-			IpAddress: ip,
+			IpAddress: ip.AsSlice(),
 			Port:      int32(port),
 		})
 	}
diff --git a/security/advancedtls/crl_test.go b/security/advancedtls/crl_test.go
index e24a8f9b..450ab561 100644
--- a/security/advancedtls/crl_test.go
+++ b/security/advancedtls/crl_test.go
@@ -30,6 +30,7 @@ import (
 	"fmt"
 	"math/big"
 	"net"
+	"net/netip"
 	"os"
 	"path"
 	"strings"
@@ -463,7 +464,7 @@ func setupTLSConn(t *testing.T) (net.Listener, *x509.Certificate, *ecdsa.Private
 		Subject:               pkix.Name{CommonName: "test-cert"},
 		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
-		IPAddresses:           []net.IP{net.ParseIP("::1")},
+		IPAddresses:           []net.IP{netip.MustParseAddr("::1").AsSlice()},
 		CRLDistributionPoints: []string{"http://static.corp.google.com/crl/campus-sln/borg"},
 	}
 
diff --git a/test/local_creds_test.go b/test/local_creds_test.go
index ac32ab52..241f6306 100644
--- a/test/local_creds_test.go
+++ b/test/local_creds_test.go
@@ -22,6 +22,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"net/netip"
 	"strings"
 	"testing"
 	"time"
@@ -181,11 +182,11 @@ func testLocalCredsE2EFail(dopts []grpc.DialOption) error {
 
 	var fakeClientAddr, fakeServerAddr net.Addr
 	fakeClientAddr = &net.IPAddr{
-		IP:   net.ParseIP("10.8.9.10"),
+		IP:   netip.MustParseAddr("10.8.9.10").AsSlice(),
 		Zone: "",
 	}
 	fakeServerAddr = &net.IPAddr{
-		IP:   net.ParseIP("10.8.9.11"),
+		IP:   netip.MustParseAddr("10.8.9.11").AsSlice(),
 		Zone: "",
 	}
 
diff --git a/xds/internal/xdsclient/xdsresource/filter_chain_test.go b/xds/internal/xdsclient/xdsresource/filter_chain_test.go
index 1897e387..cdcc8660 100644
--- a/xds/internal/xdsclient/xdsresource/filter_chain_test.go
+++ b/xds/internal/xdsclient/xdsresource/filter_chain_test.go
@@ -22,6 +22,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"net/netip"
 	"strings"
 	"testing"
 
@@ -2438,7 +2439,7 @@ func (s) TestLookup_Successes(t *testing.T) {
 			lis:  lisWithoutDefaultChain,
 			params: FilterChainLookupParams{
 				IsUnspecifiedListener: true,
-				DestAddr:              net.ParseIP("2001:68::db8"),
+				DestAddr:              netip.MustParseAddr("2001:68::db8").AsSlice(),
 				SourceAddr:            net.IPv4(10, 1, 1, 1),
 				SourcePort:            1,
 			},
@@ -2468,8 +2469,8 @@ func (s) TestLookup_Successes(t *testing.T) {
 			lis:  lisWithoutDefaultChain,
 			params: FilterChainLookupParams{
 				IsUnspecifiedListener: true,
-				DestAddr:              net.ParseIP("2001:68::1"),
-				SourceAddr:            net.ParseIP("2001:68::2"),
+				DestAddr:              netip.MustParseAddr("2001:68::1").AsSlice(),
+				SourceAddr:            netip.MustParseAddr("2001:68::2").AsSlice(),
 				SourcePort:            1,
 			},
 			wantFC: &FilterChain{