Repository: grpc-ecosystem/go-grpc-middleware
Pull request #692 (2024-02-01 11:50:39)

Extend realip parsing of GRPC peer address to handle IPv6
## Changes

Use `netip.ParseAddrPort` instead of `strings.Split` to support extraction of both ipv4 and ipv6.

## Verification

The `interceptors/realip/realip_test.go` file was extended with additional cases.



Patch:
diff --git a/interceptors/realip/realip.go b/interceptors/realip/realip.go
index 8d5a021..41b2cde 100644
--- a/interceptors/realip/realip.go
+++ b/interceptors/realip/realip.go
@@ -83,10 +83,12 @@ func getRemoteIP(ctx context.Context, trustedPeers []netip.Prefix, headers []str
 		return noIP
 	}
 
-	ip, err := netip.ParseAddr(strings.Split(pr.String(), ":")[0])
+	addrPort, err := netip.ParseAddrPort(pr.String())
 	if err != nil {
 		return noIP
 	}
+	ip := addrPort.Addr()
+
 	if len(trustedPeers) == 0 || !ipInNets(ip, trustedPeers) {
 		return ip
 	}


Test patch:
diff --git a/interceptors/realip/realip_test.go b/interceptors/realip/realip_test.go
index 4eeac51..9666ed5 100644
--- a/interceptors/realip/realip_test.go
+++ b/interceptors/realip/realip_test.go
@@ -19,17 +19,22 @@ import (
 var (
 	localnet []netip.Prefix = []netip.Prefix{
 		netip.MustParsePrefix("127.0.0.1/8"),
+		netip.MustParsePrefix("::1/128"),
 	}
 
 	privatenet []netip.Prefix = []netip.Prefix{
 		netip.MustParsePrefix("10.0.0.0/8"),
 		netip.MustParsePrefix("172.16.0.0/12"),
 		netip.MustParsePrefix("192.168.0.0/16"),
+		netip.MustParsePrefix("2002:c0a8::/32"),
 	}
 
-	privateIP netip.Addr = netip.MustParseAddr("192.168.0.1")
-	publicIP  netip.Addr = netip.MustParseAddr("8.8.8.8")
-	localhost netip.Addr = netip.MustParseAddr("127.0.0.1")
+	privateIP  netip.Addr = netip.MustParseAddr("192.168.0.1")
+	privateIP6 netip.Addr = netip.MustParseAddr("::ffff:c0a8:1")
+	publicIP   netip.Addr = netip.MustParseAddr("8.8.8.8")
+	publicIP6  netip.Addr = netip.MustParseAddr("::ffff:808:808")
+	localhost  netip.Addr = netip.MustParseAddr("127.0.0.1")
+	localhost6 netip.Addr = netip.MustParseAddr("::1")
 )
 
 func localhostPeer() *peer.Peer {
@@ -40,6 +45,14 @@ func localhostPeer() *peer.Peer {
 	}
 }
 
+func localhost6Peer() *peer.Peer {
+	return &peer.Peer{
+		Addr: &net.TCPAddr{
+			IP: net.ParseIP(localhost6.String()),
+		},
+	}
+}
+
 func publicPeer() *peer.Peer {
 	return &peer.Peer{
 		Addr: &net.TCPAddr{
@@ -56,6 +69,14 @@ func privatePeer() *peer.Peer {
 	}
 }
 
+func private6Peer() *peer.Peer {
+	return &peer.Peer{
+		Addr: &net.TCPAddr{
+			IP: net.ParseIP(privateIP6.String()),
+		},
+	}
+}
+
 type testCase struct {
 	trustedPeers []netip.Prefix
 	headerKeys   []string
@@ -331,6 +352,37 @@ func TestInterceptor(t *testing.T) {
 			testStreamServerInterceptor(t, tc)
 		})
 	})
+	t.Run("ipv6 from grpc peer", func(t *testing.T) {
+		tc := testCase{
+			trustedPeers: localnet,
+			headerKeys:   []string{},
+			peer:         localhost6Peer(),
+			expectedIP:   localhost6,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("ipv6 from header", func(t *testing.T) {
+		tc := testCase{
+			trustedPeers: privatenet,
+			headerKeys:   []string{XForwardedFor},
+			inputHeaders: map[string]string{
+				XForwardedFor: publicIP6.String(),
+			},
+			peer:       private6Peer(),
+			expectedIP: publicIP6,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
 	t.Run("unix", func(t *testing.T) {
 		tc := testCase{
 			trustedPeers: localnet,