Repository: grpc-ecosystem/go-grpc-middleware
Pull request #682 (2023-11-25 22:11:17)

Middleware for determining the real ip of the client
<!--
    Keep PR title verbose enough.
-->

## Changes

This PR adds a middleware which will determine the real ip / original IP of the client based on header values and inject this into the context of the request. A function is also introduced to allow retrieval of this value from the context at a later stage.

When a server is behind a load balancer it is often useful to attempt to determine the IP of the original client so as to not only log the IP of the load balancer. Solutions such as NGINX, AWS ELB and Google Cloud Load Balancers all provide this by appending the IP of the client to an `x-forwarded-for` header. Cloudflare provides this by setting the `True-Client-IP` header to the value of the client.

Only gRPC servers being served over TCP/IP networks are supported.

Implementation of this functionality carries with it the risk of trusting unverified user input and as such allowing the spoofing of source IP's or Denial of Service attacks. Because of this I believe this project should provide a verified and trusted implementation. Such an implementation is included in this PR.

I opted to use a table-driven approach to testing this - I'm unsure if this is acceptable for this repo. If not, please let me know and I'll refactor it.

## Verification

Unit testing with 100% coverage has been implemented. Additionally, I believe I've covered the majority of the edge-cases in testing.

From a security standpoint, the tests verifies that headers are only referenced if the request is coming from a trusted peer. In the event that a trusted peer sends a malformed header, the header is rejected (which is also covered by the tests).


MadsRC: > This is an _excellent_ PR, thank you! Just a few minor suggestions, let me know what you think.

Thank you! I've implemented the suggestions you had. Turns out the `netip.Addr` rewrite was a bit more involved, but in the end it turned out great. I also rewrote the structure of the tests.

Patch:
diff --git a/interceptors/realip/doc.go b/interceptors/realip/doc.go
new file mode 100644
index 0000000..19f3d52
--- /dev/null
+++ b/interceptors/realip/doc.go
@@ -0,0 +1,95 @@
+// Copyright (c) The go-grpc-middleware Authors.
+// Licensed under the Apache License 2.0.
+
+/*
+Package realip is a middleware that extracts the real IP of requests based on
+header values.
+
+The real IP is subsequently placed inside the context of each request and can
+be retrieved using the [FromContext] function.
+
+The middleware is designed to work with gRPC servers serving clients over
+TCP/IP connections. If no headers are found, the middleware will return the
+remote peer address as the real IP. If remote peer address is not a TCP/IP
+address, the middleware will return nil as the real IP.
+
+Headers provided by clients in the request will be searched for in the order
+of the list provided to the middleware. The first header that contains a valid
+IP address will be used as the real IP.
+
+Comma separated headers are supported. The last, rightmost, IP address in the
+header will be used as the real IP.
+
+# Security
+
+There are 2 main security concerns when deriving the real IP from request
+headers:
+
+ 1. Risk of spoofing the real IP by setting a header value.
+ 2. Risk of injecting a header value that causes a denial of service.
+
+To mitigate the risk of spoofing, the middleware introduces the concept of
+"trusted peers". Trusted peers are defined as a list of IP networks that are
+verified by the gRPC server operator to be trusted. If the peer address is found
+to be within one of the trusted networks, the middleware will attempt to extract
+the real IP from the request headers. If the peer address is not found to be
+within one of the trusted networks, the peer address will be returned as the
+real IP.
+
+"trusted" in this context means that the peer is configured to overwrite the
+header value with the real IP. This is typically done by a proxy or load
+balancer that is configured to forward the real IP of the client in a header
+value. Alternatively, the peer may be configured to append the real IP to the
+header value. In this case, the middleware will use the last, rightmost, IP
+address in the header as the real IP. Most load balancers, such as NGINX, AWS
+ELB, and Google Cloud Load Balancer, are configured to append the real IP to
+the header value as their default action.
+
+To mitigate the risk of a denial of service by proxy of a malicious header,
+the middleware validates that the header value contains a valid IP address. Only
+if a valid IP address is found will the middleware use that value as the real
+IP.
+
+# Individual IP addresses as trusted peers
+
+When creating the list of trusted peers, it is possible to specify individual IP
+addresses. This is useful when your proxy or load balancer has a set of
+well-known addresses.
+
+The following example shows how to specify individual IP addresses as trusted
+peers:
+
+	trusted := []net.IPNet{
+	    {IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)},
+	    {IP: net.IPv4(192, 168, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},
+	}
+
+In the above example, the middleware will only attempt to extract the real IP
+from the request headers if the peer address is either 192.168.0.1 or
+192.168.0.2.
+
+# Headers
+
+Headers to search for are specified as a list of strings when creating the
+middleware. The middleware will search for the headers in the order specified
+and use the first header that contains a valid IP address as the real IP.
+
+The following are examples of headers that may contain the real IP:
+
+  - X-Forwarded-For: This header is set by proxies and contains a comma
+    separated list of IP addresses. Each proxy that forwards the request will
+    append the real IP to the header value.
+  - X-Real-IP: This header is set by NGINX and contains the real IP as a string
+    containing a single IP address.
+  - Forwarded-For: Header defined by RFC7239. This header is set by proxies and
+    contains the real IP as a string containing a single IP address. Please note
+    that the obfuscated identifier from section 6.3 of RFC7239, and that the
+    unknown identifier from section 6.2 of RFC7239 are not supported.
+  - True-Client-IP: This header is set by Cloudflare and contains the real IP
+    as a string containing a single IP address.
+
+# Usage
+
+Please see examples for simple examples of use.
+*/
+package realip
diff --git a/interceptors/realip/realip.go b/interceptors/realip/realip.go
new file mode 100644
index 0000000..8d5a021
--- /dev/null
+++ b/interceptors/realip/realip.go
@@ -0,0 +1,136 @@
+// Copyright (c) The go-grpc-middleware Authors.
+// Licensed under the Apache License 2.0.
+
+package realip
+
+import (
+	"context"
+	"net"
+	"net/netip"
+	"strings"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/peer"
+)
+
+// XRealIp, XForwardedFor and TrueClientIp are header keys
+// used to extract the real client IP from the request. They represent common
+// conventions for identifying the originating IP address of a client connecting
+// through proxies or load balancers.
+const (
+	XRealIp       = "X-Real-IP"
+	XForwardedFor = "X-Forwarded-For"
+	TrueClientIp  = "True-Client-IP"
+)
+
+var noIP = netip.Addr{}
+
+type realipKey struct{}
+
+// FromContext extracts the real client IP from the context.
+// It returns the IP and a boolean indicating if it was present.
+func FromContext(ctx context.Context) (netip.Addr, bool) {
+	ip, ok := ctx.Value(realipKey{}).(netip.Addr)
+	return ip, ok
+}
+
+func remotePeer(ctx context.Context) net.Addr {
+	pr, ok := peer.FromContext(ctx)
+	if !ok {
+		return nil
+	}
+	return pr.Addr
+}
+
+func ipInNets(ip netip.Addr, nets []netip.Prefix) bool {
+	for _, n := range nets {
+		if n.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+func getHeader(ctx context.Context, key string) string {
+	md, ok := metadata.FromIncomingContext(ctx)
+	if !ok {
+		return ""
+	}
+
+	if md[strings.ToLower(key)] == nil {
+		return ""
+	}
+
+	return md[strings.ToLower(key)][0]
+}
+
+func ipFromHeaders(ctx context.Context, headers []string) netip.Addr {
+	for _, header := range headers {
+		a := strings.Split(getHeader(ctx, header), ",")
+		h := strings.TrimSpace(a[len(a)-1])
+		ip, err := netip.ParseAddr(h)
+		if err == nil {
+			return ip
+		}
+	}
+	return noIP
+}
+
+func getRemoteIP(ctx context.Context, trustedPeers []netip.Prefix, headers []string) netip.Addr {
+	pr := remotePeer(ctx)
+	if pr == nil {
+		return noIP
+	}
+
+	ip, err := netip.ParseAddr(strings.Split(pr.String(), ":")[0])
+	if err != nil {
+		return noIP
+	}
+	if len(trustedPeers) == 0 || !ipInNets(ip, trustedPeers) {
+		return ip
+	}
+	if ip := ipFromHeaders(ctx, headers); ip != noIP {
+		return ip
+	}
+	// No ip from the headers, return the peer ip.
+	return ip
+}
+
+type serverStream struct {
+	grpc.ServerStream
+	ctx context.Context
+}
+
+func (s *serverStream) Context() context.Context {
+	return s.ctx
+}
+
+// UnaryServerInterceptor returns a new unary server interceptor that extracts the real client IP from request headers.
+// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
+// The real IP is added to the request context.
+func UnaryServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.UnaryServerInterceptor {
+	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
+		ip := getRemoteIP(ctx, trustedPeers, headers)
+		if ip != noIP {
+			ctx = context.WithValue(ctx, realipKey{}, ip)
+		}
+		return handler(ctx, req)
+	}
+}
+
+// StreamServerInterceptor returns a new stream server interceptor that extracts the real client IP from request headers.
+// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
+// The real IP is added to the request context.
+func StreamServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.StreamServerInterceptor {
+	return func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
+		ip := getRemoteIP(stream.Context(), trustedPeers, headers)
+		if ip != noIP {
+			return handler(srv, &serverStream{
+				ServerStream: stream,
+				ctx:          context.WithValue(stream.Context(), realipKey{}, ip),
+			})
+		}
+		return handler(srv, stream)
+	}
+}


Test patch:
diff --git a/interceptors/realip/examples_test.go b/interceptors/realip/examples_test.go
new file mode 100644
index 0000000..3caf0d0
--- /dev/null
+++ b/interceptors/realip/examples_test.go
@@ -0,0 +1,43 @@
+// Copyright (c) The go-grpc-middleware Authors.
+// Licensed under the Apache License 2.0.
+
+package realip_test
+
+import (
+	"net/netip"
+
+	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
+	"google.golang.org/grpc"
+)
+
+// Simple example of a unary server initialization code.
+func ExampleUnaryServerInterceptor() {
+	// Define list of trusted peers from which we accept forwarded-for and
+	// real-ip headers.
+	trustedPeers := []netip.Prefix{
+		netip.MustParsePrefix("127.0.0.1/32"),
+	}
+	// Define headers to look for in the incoming request.
+	headers := []string{realip.XForwardedFor, realip.XRealIp}
+	_ = grpc.NewServer(
+		grpc.ChainUnaryInterceptor(
+			realip.UnaryServerInterceptor(trustedPeers, headers),
+		),
+	)
+}
+
+// Simple example of a streaming server initialization code.
+func ExampleStreamServerInterceptor() {
+	// Define list of trusted peers from which we accept forwarded-for and
+	// real-ip headers.
+	trustedPeers := []netip.Prefix{
+		netip.MustParsePrefix("127.0.0.1/32"),
+	}
+	// Define headers to look for in the incoming request.
+	headers := []string{realip.XForwardedFor, realip.XRealIp}
+	_ = grpc.NewServer(
+		grpc.ChainStreamInterceptor(
+			realip.StreamServerInterceptor(trustedPeers, headers),
+		),
+	)
+}
diff --git a/interceptors/realip/realip_test.go b/interceptors/realip/realip_test.go
new file mode 100644
index 0000000..4eeac51
--- /dev/null
+++ b/interceptors/realip/realip_test.go
@@ -0,0 +1,368 @@
+// Copyright (c) The go-grpc-middleware Authors.
+// Licensed under the Apache License 2.0.
+
+package realip
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"net/netip"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/peer"
+)
+
+var (
+	localnet []netip.Prefix = []netip.Prefix{
+		netip.MustParsePrefix("127.0.0.1/8"),
+	}
+
+	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"),
+	}
+
+	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")
+)
+
+func localhostPeer() *peer.Peer {
+	return &peer.Peer{
+		Addr: &net.TCPAddr{
+			IP: net.ParseIP(localhost.String()),
+		},
+	}
+}
+
+func publicPeer() *peer.Peer {
+	return &peer.Peer{
+		Addr: &net.TCPAddr{
+			IP: net.ParseIP(publicIP.String()),
+		},
+	}
+}
+
+func privatePeer() *peer.Peer {
+	return &peer.Peer{
+		Addr: &net.TCPAddr{
+			IP: net.ParseIP(privateIP.String()),
+		},
+	}
+}
+
+type testCase struct {
+	trustedPeers []netip.Prefix
+	headerKeys   []string
+	inputHeaders map[string]string
+	peer         *peer.Peer
+	expectedIP   netip.Addr
+}
+
+func testUnaryServerInterceptor(t *testing.T, c testCase) {
+	interceptor := UnaryServerInterceptor(c.trustedPeers, c.headerKeys)
+	handler := func(ctx context.Context, req any) (any, error) {
+		ip, _ := FromContext(ctx)
+
+		assert.Equal(t, c.expectedIP, ip)
+		return nil, nil
+	}
+	info := &grpc.UnaryServerInfo{
+		FullMethod: "FakeMethod",
+	}
+	ctx := context.Background()
+	if c.peer != nil {
+		ctx = peer.NewContext(ctx, c.peer)
+	}
+	if c.inputHeaders != nil {
+		md := metadata.New(c.inputHeaders)
+		ctx = metadata.NewIncomingContext(ctx, md)
+	}
+
+	resp, err := interceptor(ctx, nil, info, handler)
+	assert.Nil(t, resp)
+	assert.NoError(t, err)
+}
+
+func testStreamServerInterceptor(t *testing.T, c testCase) {
+	interceptor := StreamServerInterceptor(c.trustedPeers, c.headerKeys)
+	handler := func(srv any, stream grpc.ServerStream) error {
+		ip, _ := FromContext(stream.Context())
+
+		assert.Equal(t, c.expectedIP, ip)
+		return nil
+	}
+	info := &grpc.StreamServerInfo{
+		FullMethod: "FakeMethod",
+	}
+	ctx := context.Background()
+	if c.peer != nil {
+		ctx = peer.NewContext(ctx, c.peer)
+	}
+	if c.inputHeaders != nil {
+		md := metadata.New(c.inputHeaders)
+		ctx = metadata.NewIncomingContext(ctx, md)
+	}
+
+	err := interceptor(nil, &serverStream{ctx: ctx}, info, handler)
+	assert.NoError(t, err)
+}
+
+func TestInterceptor(t *testing.T) {
+	t.Run("no peer", func(t *testing.T) {
+		tc := testCase{
+			// Test that if there is no peer, we don't get an IP.
+			trustedPeers: localnet,
+			headerKeys:   []string{XForwardedFor},
+			inputHeaders: map[string]string{
+				XForwardedFor: localhost.String(),
+			},
+			peer:       nil,
+			expectedIP: netip.Addr{},
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+
+	t.Run("trusted peer header csv", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the remote peer is trusted and the header contains
+			// a comma separated list of valid IPs, we get right most one.
+			trustedPeers: localnet,
+			headerKeys:   []string{XForwardedFor},
+			inputHeaders: map[string]string{
+				XForwardedFor: fmt.Sprintf("%s,%s", localhost.String(), publicIP.String()),
+			},
+			peer:       localhostPeer(),
+			expectedIP: publicIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("trusted peer single", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the remote peer is trusted and the header contains
+			// a single valid IP, we get that IP.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp},
+			inputHeaders: map[string]string{
+				XRealIp: privateIP.String(),
+			},
+			peer:       localhostPeer(),
+			expectedIP: privateIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("trusted peer multiple", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the trusted peers list is larger than 1 network and
+			// the remote peer is in the third network, we get the right IP.
+			trustedPeers: privatenet,
+			headerKeys:   []string{TrueClientIp},
+			inputHeaders: map[string]string{
+				TrueClientIp: publicIP.String(),
+			},
+			peer:       privatePeer(),
+			expectedIP: publicIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("untrusted peer single", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the remote peer is not trusted and the header
+			// contains a single valid IP, we get that the peer IP.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp},
+			inputHeaders: map[string]string{
+				XRealIp: privateIP.String(),
+			},
+			peer:       publicPeer(),
+			expectedIP: publicIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("trusted peer multiple headers", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the peer is trusted and several headers are
+			// provided, the interceptor reads the IP from the first header in
+			// the list.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp, TrueClientIp},
+			inputHeaders: map[string]string{
+				XRealIp:      privateIP.String(),
+				TrueClientIp: publicIP.String(),
+			},
+			peer:       localhostPeer(),
+			expectedIP: privateIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("trusted peer multiple header configured single provided", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the peer is trusted and several headers are
+			// configured, but only one is provided, the interceptor reads the
+			// IP from the provided header.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp, TrueClientIp, XForwardedFor},
+			inputHeaders: map[string]string{
+				TrueClientIp: publicIP.String(),
+			},
+			peer:       localhostPeer(),
+			expectedIP: publicIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("trusted peer multiple header configured none provided", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the peer is trusted and several headers are, but no
+			// header is provided, the interceptor reads the IP from the peer.
+			//
+			// This indicates that the proxy is not configured to forward the
+			// IP. Using the peer IP is better than nothing.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp, TrueClientIp, XForwardedFor},
+			peer:         localhostPeer(),
+			expectedIP:   localhost,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("untrusted peer multiple headers", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the peer is not trusted, but several headers are
+			// provided, the interceptor reads the IP from peer.
+			trustedPeers: nil,
+			inputHeaders: map[string]string{
+				XRealIp:      privateIP.String(),
+				TrueClientIp: localhost.String(),
+			},
+			peer:       publicPeer(),
+			expectedIP: publicIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("untrusted peer multiple header configured single provided", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the peer is not trusted and several headers are
+			// configured, but only one is provided, the interceptor reads the
+			// IP from the peer.
+			//
+			// This is because the peer is untrusted, and as such we cannot
+			// trust the headers.
+			trustedPeers: nil,
+			headerKeys:   []string{XRealIp, TrueClientIp, XForwardedFor},
+			inputHeaders: map[string]string{
+				TrueClientIp: publicIP.String(),
+			},
+			peer:       publicPeer(),
+			expectedIP: publicIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("trusted peer malformed header", func(t *testing.T) {
+		tc := testCase{
+			// Test that if the peer is trusted, but the provided headers
+			// contain malformed IP addresses, the interceptor reads the IP
+			// from the peer.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp, TrueClientIp, XForwardedFor},
+			inputHeaders: map[string]string{
+				TrueClientIp: "malformed",
+			},
+			peer:       localhostPeer(),
+			expectedIP: localhost,
+		}
+		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,
+			headerKeys:   []string{XRealIp},
+			peer: &peer.Peer{
+				Addr: &net.UnixAddr{Name: "unix", Net: "unix"},
+			},
+			expectedIP: netip.Addr{},
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+	t.Run("header casing", func(t *testing.T) {
+		tc := testCase{
+			// Test that header casing is ignored.
+			trustedPeers: localnet,
+			headerKeys:   []string{XRealIp},
+			inputHeaders: map[string]string{
+				"X-Real-IP": privateIP.String(),
+			},
+			peer:       localhostPeer(),
+			expectedIP: privateIP,
+		}
+		t.Run("unary", func(t *testing.T) {
+			testUnaryServerInterceptor(t, tc)
+		})
+		t.Run("stream", func(t *testing.T) {
+			testStreamServerInterceptor(t, tc)
+		})
+	})
+}