Skip to content

Commit f2589df

Browse files
committed
add pointers about using interceptors and details about how to do authn/authz to readme
1 parent 6bc5579 commit f2589df

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

README.md

+118
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ underlying stream. If any RPCs are in progress on the channel when it is closed,
167167
they will be cancelled. The channel is also closed if the context used to create
168168
the stream is cancelled or times out.
169169

170+
To use client interceptors with these channels, wrap them using
171+
[`grpchan.InterceptClientConn`](https://pkg.go.dev/github.com/fullstorydev/grpchan#InterceptClientConn)
172+
before creating stubs.
173+
170174
### Server
171175

172176
To handle RPCs that are issued over tunnels, the server must register service
@@ -190,6 +194,25 @@ barpb.RegisterBarServer(handler, serviceImpl)
190194
barpb.RegisterBarServer(svr, serviceImpl)
191195
```
192196

197+
To use server interceptors with these handlers, wrap the `TunnelServiceHandler`
198+
using [`grpchan.WithInterceptor`](https://pkg.go.dev/github.com/fullstorydev/grpchan#WithInterceptor)
199+
before registering the other handlers.
200+
201+
### Authn/Authz
202+
203+
With forward tunnels, authentication can be done on the initial `OpenTunnel` RPC
204+
that opens the tunnel. The identity of the client can then be stored in a
205+
context value, from a server interceptor. These context values are also
206+
available to server interceptors and handlers that process tunneled requests.
207+
So an authorization interceptor could extract the client identity from the
208+
request context.
209+
210+
If using mutual TLS, you can use `peer.FromContext` (part of the gRPC runtime)
211+
to examine the client's identity, which would have been authenticated via client
212+
certificate. Like other context values, this value is available to all server
213+
interceptors and handlers of tunneled requests and will be the same peer that
214+
opened the tunnel.
215+
193216
## Reverse Tunnels
194217

195218
A reverse tunnel is one in which the tunnel client is actually the network
@@ -253,6 +276,10 @@ The `Serve` function returns once the tunnel is closed, either via the
253276
tunnel client closing the channel or some other interruption of the
254277
stream (including the context being cancelled or timing out).
255278

279+
To use server interceptors with these handlers, wrap the `ReverseTunnelServer`
280+
using [`grpchan.WithInterceptor`](https://pkg.go.dev/github.com/fullstorydev/grpchan#WithInterceptor)
281+
before registering the other handlers.
282+
256283
### Server
257284

258285
The network server for reverse services is where things get really interesting.
@@ -300,3 +327,94 @@ most usages:
300327

301328
All of these channels can be used just like a `*grpc.ClientConn`, for creating
302329
RPC stubs and then issuing RPCs to the corresponding network client.
330+
331+
To use client interceptors with these channels, wrap them using
332+
[`grpchan.InterceptClientConn`](https://pkg.go.dev/github.com/fullstorydev/grpchan#InterceptClientConn)
333+
before creating stubs.
334+
335+
### Authn/Authz
336+
337+
With reverse tunnels, authentication is a little different than with forward
338+
tunnels. The credentials associated with the initial `OpenReverseTunnel` RPC
339+
are those for the tunnel _server_. Unless you are using mutual TLS (where
340+
both parties authenticate via certificate), you will need to supply additional
341+
authentication material with tunneled requests.
342+
343+
One way to send authentication material is to have the client (which is actually
344+
the network server) use client interceptors to include per-call credentials with
345+
every request. This approach closely resembles how non-tunneled RPCs are
346+
handled: both sides use interceptors to send and verify credentials with every
347+
operation.
348+
349+
A more efficient way involves only authenticating once, since all calls over the
350+
tunnel will have the same authenticated client. This can be done by having a
351+
server interceptor that sends authentication materials in _response_ headers.
352+
These will be received by the client almost immediately after the tunnel is
353+
opened. This identity can then be stored in the context using a mutable value:
354+
355+
```go
356+
// Client interceptor for the OpenReverseTunnel RPC:
357+
func reverseCredentialsInterceptor(
358+
ctx context.Context,
359+
desc *grpc.StreamDesc,
360+
cc *grpc.ClientConn,
361+
method string,
362+
streamer grpc.Streamer,
363+
opts ...grpc.CallOption,
364+
) (grpc.ClientStream, error) {
365+
if method != "/grpctunnel.v1.TunnelService/OpenReverseTunnel" {
366+
return streamer(ctx, desc, cc, method, opts...)
367+
}
368+
// Store mutable value in context.
369+
var authInfo any
370+
ctx = context.WithValue(ctx, reverseCredentialsKey{}, &authInfo)
371+
// Invoke RPC; open the tunnel.
372+
stream, err := streamer(ctx, desc, cc, method, opts...)
373+
if err != nil {
374+
return nil, err
375+
}
376+
// Get credentials from response headers.
377+
md, err := stream.Header()
378+
if err != nil {
379+
return nil, err
380+
}
381+
// If authentication fails, authInfo could include details about
382+
// the failure so that tunneled RPCs can fail with appropriate
383+
// error details.
384+
//
385+
// An alternative is to just close the tunnel immediately, right
386+
// here. But then there is no way to send information about the
387+
// authn error to the peer.
388+
//
389+
// Note that modifying authInfo here is okay. But it is not safe
390+
// to modify after returning from this interceptor since that
391+
// could lead to data races with tunneled RPCs reading it.
392+
authInfo = authenticate(md)
393+
return stream, nil
394+
}
395+
396+
// Server interceptor for tunneled RPCs.
397+
// (Unary interceptor shown; streaming interceptor would be similar.)
398+
func tunneledAuthzInterceptor(
399+
ctx context.Context,
400+
method string,
401+
req, reply interface{},
402+
cc *grpc.ClientConn,
403+
invoker grpc.UnaryInvoker,
404+
opts ...grpc.CallOption,
405+
) error {
406+
// Get authInfo from context. This was stored in the context by
407+
// the interceptor above. More detailed error messages could be
408+
// used or error details added if authInfo contains details about
409+
// authn failures.
410+
authInfo, ok := ctx.Value(reverseCredentialsKey{}).(*any)
411+
if !ok || authInfo == nil || *authInfo == nil {
412+
return status.Error(codes.Unauthenticated, "unauthenticated")
413+
}
414+
if !isAllowed(method, *authInfo) {
415+
return status.Error(codes.PermissionDenied, "not authorized")
416+
}
417+
// RPC is allowed.
418+
return invoker(ctx, method, req, reply, cc, opts...)
419+
}
420+
```

service.go

+6
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ func (s *TunnelServiceHandler) openReverseTunnel(stream tunnelpb.TunnelService_O
126126
return status.Error(codes.Unimplemented, "reverse tunnels not supported")
127127
}
128128

129+
// Immediately send headers instead of waiting for first RPC to send a message.
130+
// This gives any server interceptors a chance to run and potentially to send
131+
// auth credentials in response headers (since the client will need a way to
132+
// authenticate the server, since roles are reversed with reverse tunnels).
133+
_ = stream.SendHeader(nil)
134+
129135
ch := newReverseChannel(stream, s.unregister)
130136
defer ch.Close()
131137

0 commit comments

Comments
 (0)