{"id":16766107,"url":"https://github.com/jhump/grpctunnel","last_synced_at":"2025-04-06T06:07:53.135Z","repository":{"id":57551758,"uuid":"155726726","full_name":"jhump/grpctunnel","owner":"jhump","description":"gRPC Tunneling","archived":false,"fork":false,"pushed_at":"2025-02-26T01:46:49.000Z","size":170,"stargazers_count":105,"open_issues_count":1,"forks_count":8,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-30T05:04:26.360Z","etag":null,"topics":["go","golang","grpc","tunneling"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jhump.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-11-01T14:21:43.000Z","updated_at":"2025-03-09T19:41:42.000Z","dependencies_parsed_at":"2023-10-20T02:54:38.909Z","dependency_job_id":"7cab620f-d25c-4233-b66f-34139d2fcea0","html_url":"https://github.com/jhump/grpctunnel","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhump%2Fgrpctunnel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhump%2Fgrpctunnel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhump%2Fgrpctunnel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhump%2Fgrpctunnel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jhump","download_url":"https://codeload.github.com/jhump/grpctunnel/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247441051,"owners_count":20939239,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","grpc","tunneling"],"created_at":"2024-10-13T06:05:23.795Z","updated_at":"2025-04-06T06:07:53.115Z","avatar_url":"https://github.com/jhump.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gRPC Tunnels\n[![Build Status](https://circleci.com/gh/jhump/grpctunnel/tree/main.svg?style=svg)](https://circleci.com/gh/jhump/grpctunnel/tree/main)\n[![Go Report Card](https://goreportcard.com/badge/github.com/jhump/grpctunnel)](https://goreportcard.com/report/github.com/jhump/grpctunnel)\n[![GoDoc](https://godoc.org/github.com/jhump/grpctunnel?status.svg)](https://godoc.org/github.com/jhump/grpctunnel)\n\nThis library enables carrying gRPC over gRPC. There are a few niche use cases\nwhere this could be useful, but the most widely applicable one is likely for\nletting gRPC servers communicate in the reverse direction, sending requests to\nconnected clients.\n\nThe tunnel is itself a gRPC service, which provides bidirectional streaming\nmethods for forward and reverse tunneling.\n\n * **Forward Tunnel**: A forward tunnel is the same direction as normal\n   gRPC connections. This means that the gRPC client that created the tunnel\n   is also the client for requests that flow through the tunnel. For forward\n   cases, the server must register service handlers for the gRPC services\n   that can be accessed over the tunnel.\n\n   A forward tunnel allows for all requests made on the tunnel to be directed\n   to the same server. With a typical gRPC client, connecting to a replicated\n   server, requests are typically load balanced across backends. For typical\n   stateless applications, this is desirable for resource utilization and fault\n   tolerance. But some applications that are not stateless may need affinity.\n   The tunnel provides that affinity. Instead of the client making multiple\n   requests, which could all be directed to different backends, the client\n   makes one request to open a tunnel. The resulting tunnel can then be used\n   to create other RPC stubs, so that all requests issued via those stubs are\n   directed to the single backend to which the tunnel was opened.\n\n * **Reverse Tunnel**: A reverse tunnel is the opposite: requests flow in the\n   reverse direction of a normal gRPC connection. This means that the gRPC\n   client that created the tunnel actually acts as the server. The gRPC server\n   with which the reverse tunnel was opened acts as the client, sending requests\n   to the gRPC client.\n\n   A reverse tunnel allows for rich \"server push\"-like capabilities, where a\n   server can push data to the client (by initiating an RPC through the reverse\n   tunnel) and even get back a response. This kind of functionality can be built\n   using regular gRPC bidirectional streams, but this library provides a better\n   and more familiar abstraction. A reverse tunnel is also classically used in\n   cases where the server cannot be directly dialed due to network topology\n   (e.g. being behind NAT). In these cases, the server dials a central router\n   and registers itself, allowing the router to forward requests to the server\n   over the reverse tunnel.\n\n## Terminology\n\nTalk of tunneling can make concepts like \"client\" and \"server\" confusing, since\nreverse tunnels swap the typical roles. So in the hopes of clarity, the rest of\nthis document will use the following terms to hopefully avoid confusion:\n\n* _channel_: A conduit through which gRPC requests can be sent and responses\n  received. This can be a TCP connection but can also be a tunnel.\n* _tunnel_: A single gRPC stream that can act as a channel, carrying other\n  gRPC requests and responses over that stream.\n* _tunnel service_: The gRPC service via which a tunnel is opened. This is\n  a service named `grpctunnel.v1.TunnelService` and defines the actual\n  tunneling protocol, in the form of bidirectional streaming RPCs.\n* _tunnel handler_: The implementation of the tunnel service. This is the\n  code that handles RPCs to the `grpctunnel.v1.TunnelService` and actually\n  implements tunnels.\n* _network client_: The gRPC client that opened the tunnel. For typical TCP\n  connections, this is the TCP client.\n* _network server_: The gRPC server to which a tunnel was opened. For typical\n  TCP connections, this is the TCP server.\n* _tunnel client_: The gRPC client that initiates requests on a tunnel.\n* _tunnel server_: The gRPC server handles requests on a tunnel.\n* _forward tunnel_: A tunnel in which the network client is also the tunnel\n  client, and the network server is also the tunnel server.\n* _reverse tunnel_: A tunnel in which the network server is actually the\n  tunnel client, and the network client acts as the tunnel server.\n\n## Tunnel Handler\n\nThe tunnel handler is constructed via\n[`grpctunnel.NewTunnelServiceHandler`](https://pkg.go.dev/github.com/jhump/grpctunnel#NewTunnelServiceHandler).\nThe options provided with this call are for configuring reverse tunnel support.\nThe handler has other methods that can be used to interact with reverse tunnels\nthat have been established.\n\nThe resulting handler has a `Service` method, which returns the actual service\nimplementation, which can then be registered with a gRPC server using \n[`tunnelpb.RegisterTunnelServiceServer`](https://pkg.go.dev/github.com/jhump/grpctunnel/tunnelpb#RegisterTunnelServiceServer).\n\n```go\nhandler := grpctunnel.NewTunnelServiceHandler(\n    grpctunnel.TunnelServiceHandlerOptions{},\n)\nsvr := grpc.NewServer()\ntunnelpb.RegisterTunnelServiceServer(svr, handler.Service())\n\n// TODO: Configure services for forward tunnels.\n// TODO: Inject handler into code that will use reverse tunnels.\n\n// Start the gRPC server.\nl, err := net.Listen(\"tcp\", \"0.0.0.0:7899\")\nif err != nil {\n    log.Fatal(err)\n}\nif err := svr.Serve(l); err != nil {\n    log.Fatal(err)\n}\n```\n\n## Forward Tunnels\n\nA forward tunnel is one in which the tunnel client is the same as the network\nclient, and the tunnel server is the same as the network server. So the client\nthat opened the tunnel is also the one that initiates RPCs through the tunnel.\n```mermaid\nsequenceDiagram\n    participant network client\n    participant network server\n    network client-\u003e\u003e+network server: opens the tunnel via RPC `grpctunnel.v1.TunnelService/OpenTunnel`\n    rect rgb(245, 248, 255)\n      loop forward tunnel created\n        network client-\u003e\u003e+network server: sends RPC request over the tunnel\n\tnetwork server-\u003e\u003enetwork server: handles request\n        network server-\u003e\u003e-network client: sends RPC response over the tunnel\n      end\n    end\n    network server-\u003e\u003e-network client: tunnel closed\n```\n\n### Client\n\nThe tunnel is opened by the client using the generated stub for the tunneling\nprotocol: [`tunnelpb.TunnelServiceClient`](https://pkg.go.dev/github.com/jhump/grpctunnel/tunnelpb#TunnelServiceClient).\n\nOnce a stream has been established via the stub's `OpenTunnel` method, it can\nbe used to create a channel via [`grpctunnel.NewChannel`](https://pkg.go.dev/github.com/jhump/grpctunnel#NewChannel).\n\nThat channel can be used to create other stubs, as if it were a\n`*grpc.ClientConn`. All RPCs issued from stubs created with this channel will be\ndirected through the tunnel.\n\n```go\n// Dial the server.\ncc, err := grpc.Dial(\n\t\"127.0.0.1:7899\",\n\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n)\nif err != nil {\n\tlog.Fatal(err)\n}\n\ntunnelStub := tunnelpb.NewTunnelServiceClient(cc)\n// Opens a tunnel and return a channel.\nch, err := grpctunnel.NewChannel(tunnelStub).Start(context.Background())\nif err != nil {\n\tlog.Fatal(err)\n}\n\n// TODO: Create stubs using ch to send RPCs through the tunnel.\n```\n\nTo close the tunnel, use the channel's `Close` method. This will also close the\nunderlying stream. If any RPCs are in progress on the channel when it is closed,\nthey will be cancelled. The channel is also closed if the context passed to\n`Start` is cancelled or times out.\n\nTo use client interceptors with these channels, wrap them using\n[`grpchan.InterceptClientConn`](https://pkg.go.dev/github.com/fullstorydev/grpchan#InterceptClientConn)\nbefore creating stubs.\n\n### Server\n\nTo handle RPCs that are issued over tunnels, the server must register service\nhandlers using the `TunnelServiceHandler`.\n\n\n```go\nhandler := grpctunnel.NewTunnelServiceHandler(\n    grpctunnel.TunnelServiceHandlerOptions{},\n)\nsvr := grpc.NewServer()\ntunnelpb.RegisterTunnelServiceServer(svr, handler.Service())\n\n// Register services to be used with forward tunnels.\nfoopb.RegisterFooServer(handler, \u0026fooServer{})\n\n// To expose a service over both tunnels and non-tunnel connections, you must\n// register it with the gRPC server, too.\nserviceImpl := newBarServer()\nbarpb.RegisterBarServer(handler, serviceImpl)\nbarpb.RegisterBarServer(svr, serviceImpl)\n```\n\nTo use server interceptors with these handlers, wrap the `TunnelServiceHandler`\nusing [`grpchan.WithInterceptor`](https://pkg.go.dev/github.com/fullstorydev/grpchan#WithInterceptor)\nbefore registering the other handlers.\n\n### Authn/Authz\n\nWith forward tunnels, authentication can be done on the initial `OpenTunnel` RPC\nthat opens the tunnel. The identity of the client can then be stored in a\ncontext value, from a server interceptor. These context values are also\navailable to server interceptors and handlers that process tunneled requests.\nSo an authorization interceptor could extract the client identity from the\nrequest context.\n\nIf using mutual TLS, you can use `peer.FromContext` (part of the gRPC runtime)\nto examine the client's identity, which would have been authenticated via client\ncertificate. Like other context values, this value is available to all server\ninterceptors and handlers of tunneled requests and will be the same peer that\nopened the tunnel.\n\n## Reverse Tunnels\n\nA reverse tunnel is one in which the tunnel client is actually the network\nserver; the tunnel server is the network client. So it's the server to which the\ntunnel was opened that actually initiates RPCs through the tunnel.\n```mermaid\nsequenceDiagram\n    participant network client\n    participant network server\n    network client-\u003e\u003e+network server: opens the tunnel via RPC `grpctunnel.v1.TunnelService/OpenReverseTunnel`\n    rect rgb(245, 248, 255)\n      loop reverse tunnel created\n        network server-\u003e\u003e+network client: sends RPC request over the tunnel\n        network client-\u003e\u003enetwork client: handles request\n        network client-\u003e\u003e-network server: sends RPC response over the tunnel\n      end\n    end\n    network server-\u003e\u003e-network client: tunnel closed\n```\n\nBecause the typical roles of client and server are reversed, usage of reverse\ntunnels is a bit more complicated than usage of forward tunnels.\n\n### Client\n\nThe tunnel is opened by the client using the generated stub for the tunneling\nprotocol: [`tunnelpb.TunnelServiceClient`](https://pkg.go.dev/github.com/jhump/grpctunnel/tunnelpb#TunnelServiceClient).\n\nHowever, since the network client will act as the channel server, handlers for\nthe exposed services must be registered before the tunnel is actually created.\nThis is done by creating a [`grpctunnel.ReverseTunnelServer`](https://pkg.go.dev/github.com/jhump/grpctunnel#NewReverseTunnelServer)\nand then using it to register service implementations, just as one would\nregister service implementations with a `*grpc.Server`.\n\nOnce all services are registered, we can call `Serve` to actually open the\nreverse tunnel and accept and process RPC requests sent by the network server.\n\n```go\n// Dial the server.\ncc, err := grpc.Dial(\n\t\"127.0.0.1:7899\",\n\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n)\nif err != nil {\n\tlog.Fatal(err)\n}\n\n// Register services for reverse tunnels.\ntunnelStub := tunnelpb.NewTunnelServiceClient(cc)\nchannelServer := grpctunnel.NewReverseTunnelServer(tunnelStub)\nfoopb.RegisterFooServer(channelServer, \u0026fooServer{})\nbarpb.RegisterBarServer(channelServer, newBarServer())\n\n// Open the reverse tunnel and serve requests.\nif _, err := channelServer.Serve(context.Background()); err != nil {\n\tlog.Fatal(err)\n}\n```\n\nThe `Serve` function returns once the tunnel is closed, either via the\ntunnel client closing the channel or some other interruption of the\nstream (including the context being cancelled or timing out).\n\nTo use server interceptors with these handlers, wrap the `ReverseTunnelServer`\nusing [`grpchan.WithInterceptor`](https://pkg.go.dev/github.com/fullstorydev/grpchan#WithInterceptor)\nbefore registering the other handlers.\n\n### Server\n\nThe network server for reverse services is where things get really interesting.\nThe network server will be acting as a tunnel client. So it needs a way to\ninspect the reverse tunnels, to decide which one should be used for an RPC.\nIt is even possible to group multiple reverse tunnels so they act like a\nconnection pool, where RPCs can be scattered over multiple tunnels in a\nround-robin fashion.\n\nWhen the handler is created, it is given options that control the behavior of\nreverse tunnels:\n* `NoReverseTunnels`: Reverse tunnels can be completely disabled this way, which\n  will cause all network clients to receive a \"Not Implemented\" error if they try\n  to establish reverse tunnels.\n* `OnReverseTunnelOpen`, `OnReverseTunnelClose`: These callbacks, if provided,\n  let your application know whenever a reverse tunnel is opened or closed, to\n  track the available tunnels. Each such tunnel is a channel that can be used to\n  send RPCs to the corresponding network client.\n* `AffinityKey`: This is a function for grouping reverse tunnels. The function\n  is given a `TunnelChannel` and returns a key. The function has access to the\n  corresponding stream's context, from which it can query for properties that\n  may be useful for grouping -- such as the authenticated peer, request headers\n  provided when the tunnel was opened, and any other context values that may\n  have been populated by interceptors. All reverse tunnels with the same key can\n  be treated like a connection pool using the handler's\n  [`KeyAsChannel`](https://pkg.go.dev/github.com/jhump/grpctunnel#TunnelServiceHandler.KeyAsChannel)\n  method.\n\nThe callbacks provide the most flexibility for how to make use of the available\nreverse tunnels. But the handler has other methods that should be sufficient for\nmost usages:\n* `AsChannel()`: Returns a channel that effectively groups _all_ reverse tunnels\n  into a single connection pool. Issuing RPCs with this channel will round-robin\n  through them. This works even if no affinity key function was provided when\n  then handler was created.\n* `KeyAsChannel()`: Similar to above, but allows for selecting a subset of\n  reverse tunnels to treat as a single pool. This is only useful if an affinity\n  key function is provided when the handler is created.\n* `AllReverseTunnels()`: Returns a slice of all available reverse tunnels. This\n  allows flexibility for selecting a reverse tunnel, but at a potential\n  performance cost since it requires the caller to re-query and re-scan the\n  slice prior to issuing an RPC. (Storing the slice and using it for future RPCs\n  is risky because the slice is a snapshot that can quickly become stale: new\n  reverse tunnels may be opened and items in the slice may be closed.)\n\nAll of these channels can be used just like a `*grpc.ClientConn`, for creating\nRPC stubs and then issuing RPCs to the corresponding network client.\n\nTo use client interceptors with these channels, wrap them using\n[`grpchan.InterceptClientConn`](https://pkg.go.dev/github.com/fullstorydev/grpchan#InterceptClientConn)\nbefore creating stubs.\n\n### Authn/Authz\n\nWith reverse tunnels, authentication is a little different than with forward\ntunnels. The credentials associated with the initial `OpenReverseTunnel` RPC\nare those for the tunnel _server_. Unless you are using mutual TLS (where\nboth parties authenticate via certificate), you will need to supply additional\nauthentication material with tunneled requests.\n\nOne way to send authentication material is to have the client (which is actually\nthe network server) use client interceptors to include per-call credentials with\nevery request. This approach closely resembles how non-tunneled RPCs are\nhandled: both sides use interceptors to send and verify credentials with every\noperation.\n\nA more efficient way involves only authenticating once, since all calls over the\ntunnel will have the same authenticated client. This can be done by having a\nserver interceptor that sends authentication materials in _response_ headers.\nThese will be received by the client almost immediately after the tunnel is\nopened. This identity can then be stored in the context using a mutable value:\n\n```go\n// Client interceptor for the OpenReverseTunnel RPC:\nfunc reverseCredentialsInterceptor(\n\tctx context.Context,\n\tdesc *grpc.StreamDesc,\n\tcc *grpc.ClientConn,\n\tmethod string,\n\tstreamer grpc.Streamer,\n\topts ...grpc.CallOption,\n) (grpc.ClientStream, error) {\n\tif method != \"/grpctunnel.v1.TunnelService/OpenReverseTunnel\" {\n\t\treturn streamer(ctx, desc, cc, method, opts...)\n\t}\n\t// Store mutable value in context.\n\tvar authInfo any\n\tctx = context.WithValue(ctx, reverseCredentialsKey{}, \u0026authInfo)\n\t// Invoke RPC; open the tunnel.\n\tstream, err := streamer(ctx, desc, cc, method, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Get credentials from response headers.\n\tmd, err := stream.Header()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// If authentication fails, authInfo could include details about\n\t// the failure so that tunneled RPCs can fail with appropriate\n\t// error details.\n\t//\n\t// An alternative is to just close the tunnel immediately, right\n\t// here. But then there is no way to send information about the\n\t// authn error to the peer.\n\t//\n\t// Note that modifying authInfo here is okay. But it is not safe\n\t// to modify after returning from this interceptor since that\n\t// could lead to data races with tunneled RPCs reading it.\n\tauthInfo = authenticate(md)\n\treturn stream, nil\n}\n\n// Server interceptor for tunneled RPCs.\n// (Unary interceptor shown; streaming interceptor would be similar.)\nfunc tunneledAuthzInterceptor(\n\tctx context.Context,\n\tmethod string,\n\treq, reply interface{},\n\tcc *grpc.ClientConn,\n\tinvoker grpc.UnaryInvoker,\n\topts ...grpc.CallOption,\n) error {\n\t// Get authInfo from context. This was stored in the context by\n\t// the interceptor above. More detailed error messages could be\n\t// used or error details added if authInfo contains details about\n\t// authn failures.\n\tauthInfo, ok := ctx.Value(reverseCredentialsKey{}).(*any)\n\tif !ok || authInfo == nil || *authInfo == nil {\n\t\treturn status.Error(codes.Unauthenticated, \"unauthenticated\")\n\t}\n\tif !isAllowed(method, *authInfo) {\n\t\treturn status.Error(codes.PermissionDenied, \"not authorized\")\n\t}\n\t// RPC is allowed.\n\treturn invoker(ctx, method, req, reply, cc, opts...)\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhump%2Fgrpctunnel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjhump%2Fgrpctunnel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhump%2Fgrpctunnel/lists"}