-
Notifications
You must be signed in to change notification settings - Fork 15
infra: Add RCP proxy binary and client library #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
16878f8
Add RCP proxy and client library
frhuelsz 57e5e61
Add make rules
frhuelsz e947843
Improvements & docs
frhuelsz 70f9b55
pr comments
frhuelsz 0635000
pr comments
frhuelsz 38c711b
Update tools/cmd/rcp-proxy/main.go
frhuelsz 3a94212
pr comments
frhuelsz c0c5e66
Update tools/pkg/rcp/proxy/proxy.go
frhuelsz cb29368
pr comments
frhuelsz d104625
Merge remote-tracking branch 'origin/user/frhuelsz/grpc/rcp-proxy' in…
frhuelsz 68ee8a5
Update tools/pkg/rcp/tlscerts/generate.go
frhuelsz 1b44d15
Update tools/pkg/rcp/proxy/proxy.go
frhuelsz dd1d19d
PR comments
frhuelsz 9aa6a74
PR comments
frhuelsz 4dfdf97
Update tools/pkg/rcp/proxy/proxy.go
frhuelsz 6e0aabe
cert injection
frhuelsz 6c43076
Update tools/cmd/rcp-proxy/main.go
frhuelsz 40345ed
Merge branch 'main' into user/frhuelsz/grpc/rcp-proxy
frhuelsz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| //go:build tls_client | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/alecthomas/kong" | ||
| "github.com/sirupsen/logrus" | ||
|
|
||
| "tridenttools/pkg/rcp" | ||
| "tridenttools/pkg/rcp/proxy" | ||
| "tridenttools/pkg/rcp/tlscerts" | ||
| ) | ||
|
|
||
| var cli struct { | ||
| ClientAddress string `arg:"" help:"Address of the rcp-client to connect to"` | ||
| ServerAddress string `short:"s" long:"server-address" help:"Address of the server to connect to" default:"${defaultServerAddress}"` | ||
| } | ||
|
|
||
| func main() { | ||
| _ = kong.Parse(&cli, | ||
| kong.Name("rcp-proxy"), | ||
| kong.Description("A reverse-connect proxy that connects to an rcp-client to forward proxy connections between it and a server."), | ||
| kong.UsageOnError(), | ||
| kong.Vars{ | ||
| "defaultServerAddress": rcp.DefaultTridentSocketPath, | ||
| }, | ||
| ) | ||
|
|
||
| // Handle Ctrl+C gracefully | ||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) | ||
| defer stop() | ||
| logrus.Infof("Starting reverse-connect proxy with client address: '%s' and server address: '%s'", cli.ClientAddress, cli.ServerAddress) | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err := proxy.StartReverseConnectProxy(ctx, tlscerts.ClientCertProvider, cli.ClientAddress, cli.ServerAddress, time.Second); err != nil { | ||
| logrus.Fatalf("reverse-connect proxy error: %v", err) | ||
| } | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| logrus.Info("Shutdown complete") | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package client | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "crypto/x509" | ||
| "fmt" | ||
| "net" | ||
|
|
||
| "tridenttools/pkg/rcp/tlscerts" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| // ListenAndAccept starts a TLS listener on the specified port and waits | ||
| // (blocking) for a single incoming connection. It returns the accepted | ||
| // connection and closes the listener. | ||
| // | ||
| // If the context is cancelled before a connection is accepted, it returns the | ||
| // context's error. | ||
| // | ||
| // The caller is responsible for closing the returned connection. | ||
| // | ||
| // This function uses mutual TLS authentication, requiring clients to present | ||
| // valid certificates. This listener is intended to be used by the RCP-proxy | ||
| // built with the same TLS setup. Any other clients or certificates will be | ||
| // rejected mutually. | ||
| func ListenAndAccept(ctx context.Context, certProvider tlscerts.CertProvider, port uint32) (net.Conn, error) { | ||
| // Load our private server certificate | ||
| cer, err := certProvider.LocalCert() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to load server certificate: %w", err) | ||
| } | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Create a certificate pool and load the client public certificate | ||
| caCertPool := x509.NewCertPool() | ||
| if !caCertPool.AppendCertsFromPEM(certProvider.RemoteCertPEM()) { | ||
| return nil, fmt.Errorf("failed to load client CA certificate(s) into pool") | ||
| } | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Start a TLS listener | ||
| listener, err := tls.Listen("tcp", fmt.Sprintf(":%d", port), &tls.Config{ | ||
| Certificates: []tls.Certificate{cer}, | ||
| ClientCAs: caCertPool, | ||
| ClientAuth: tls.RequireAndVerifyClientCert, | ||
| MinVersion: tls.VersionTLS13, | ||
| }) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to listen on port %d: %w", port, err) | ||
| } | ||
| defer listener.Close() | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| logrus.Debugf("RCP-client listening on port %d", port) | ||
|
|
||
| // Create a sub-context to handle listener closure on context cancellation. | ||
| acceptCtx, cancel := context.WithCancel(ctx) | ||
| defer cancel() | ||
|
|
||
| go func() { | ||
| // In the background, wait for context cancellation to close the | ||
| // listener. This is necessary because Accept() is a blocking call so we | ||
| // need to close the listener in parallel while the parent goroutine is | ||
| // waiting. Closing the listener will cause Accept() to return an error | ||
| // which we can handle appropriately. | ||
| <-acceptCtx.Done() | ||
| listener.Close() | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }() | ||
|
|
||
| // Wait for an incoming connection | ||
| conn, err := listener.Accept() | ||
| if err != nil { | ||
| if ctx.Err() != nil { | ||
| // Context was cancelled | ||
| return nil, ctx.Err() | ||
| } | ||
|
|
||
| // Some other error occurred | ||
| return nil, fmt.Errorf("failed to accept connection: %w", err) | ||
| } | ||
|
|
||
| return conn, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package rcp | ||
|
|
||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const ( | ||
| DefaultTridentSocketPath = "/run/trident.sock" | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| package proxy | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "crypto/x509" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
|
|
||
| "tridenttools/pkg/rcp/tlscerts" | ||
| ) | ||
|
|
||
| // StartReverseConnectProxy starts a Reverse Connect Proxy that connects to a | ||
| // client at clientAddress and once a TLS connection is established, connects to | ||
| // a server at serverAddress, then proxies data between the two connections. | ||
| // | ||
| // This is a blocking call that runs until the context is cancelled. | ||
| // | ||
| // Both client and server connections use mutual TLS authentication, requiring | ||
| // valid certificates. This proxy is intended to be used by the RCP-client built | ||
| // with the same TLS setup. Any other certificates will be rejected mutually. | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| func StartReverseConnectProxy( | ||
| ctx context.Context, | ||
| certProvider tlscerts.CertProvider, | ||
| clientAddress string, | ||
| serverAddress string, | ||
| retryInterval time.Duration, | ||
| ) error { | ||
| // Load our private client certificate | ||
| cer, err := certProvider.LocalCert() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to load client certificate: %w", err) | ||
| } | ||
|
|
||
| // Create a certificate pool and load the server public certificate | ||
| caCertPool := x509.NewCertPool() | ||
| if ok := caCertPool.AppendCertsFromPEM(certProvider.RemoteCertPEM()); !ok { | ||
| return fmt.Errorf("failed to load server CA certificate(s) into pool") | ||
| } | ||
|
|
||
| // Configure TLS with mutual authentication | ||
| tlsConfig := tls.Config{ | ||
| Certificates: []tls.Certificate{cer}, | ||
| RootCAs: caCertPool, | ||
| ServerName: tlscerts.ServerSubjectAltName, | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| MinVersion: tls.VersionTLS13, | ||
| } | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Bool to keep track if the connection has been refused multiple times | ||
| // consecutively, to reduce log spam. | ||
| multipleRefused := false | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Main loop to keep trying to connect to the client | ||
| for { | ||
| if ctx.Err() != nil { | ||
| // Context cancelled, exit loop | ||
| return ctx.Err() | ||
| } | ||
|
|
||
| // Try to establish connection to the client | ||
| clientConn, err := tls.Dial("tcp", clientAddress, &tlsConfig) | ||
| if err != nil { | ||
| var errno syscall.Errno | ||
|
|
||
| // Check if this is a connection refused error | ||
| if ok := errors.As(err, &errno); ok && errno == syscall.ECONNREFUSED { | ||
| if !multipleRefused { | ||
| multipleRefused = true | ||
| logrus.Warnf("Client connection refused, will retry silently.") | ||
| } | ||
| } else { | ||
| // Some other error occurred | ||
| logrus.Errorf("Failed to establish client connection: %v", err) | ||
| } | ||
|
|
||
| // Wait before retrying | ||
| select { | ||
| case <-time.After(retryInterval): | ||
| case <-ctx.Done(): | ||
| // Context cancelled, exit loop | ||
| return ctx.Err() | ||
| } | ||
|
|
||
| continue | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Successful connection, reset refused flag | ||
| multipleRefused = false | ||
|
|
||
| // Handle the client connection, the function will block until the | ||
| // connection is closed or an error occurs and close the connection. | ||
| logrus.Infof("Client connected from '%s'", clientConn.RemoteAddr().String()) | ||
| err = handleClientConnection(ctx, clientConn, serverAddress) | ||
| if err != nil { | ||
| logrus.Errorf("Client connection error: %v", err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // handleClientConnection handles a single client connection by connecting to | ||
| // the server and proxying data between the client and server connections. | ||
| // | ||
| // This function blocks until the connection is closed or an error occurs. | ||
| func handleClientConnection(ctx context.Context, clientConn net.Conn, serverAddress string) error { | ||
| defer clientConn.Close() | ||
|
|
||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| logrus.Infof("Connecting to server at '%s'", serverAddress) | ||
| serverConn, err := net.Dial("unix", serverAddress) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to connect to server at '%s': %w", serverAddress, err) | ||
| } | ||
| defer serverConn.Close() | ||
|
|
||
| // Both connections are established, start proxying data between them | ||
|
|
||
| // Channel to signal when copying is done. Buffered to allow both goroutines to send without blocking. | ||
| doneChan := make(chan string, 2) | ||
|
|
||
| // Start the proxying | ||
| go func() { | ||
| _, err := io.Copy(serverConn, clientConn) | ||
| if err != nil { | ||
| switch { | ||
| case errors.Is(err, io.EOF), | ||
| errors.Is(err, net.ErrClosed), | ||
| errors.Is(err, syscall.EPIPE): | ||
| logrus.Debugf("Connection closed while copying from client to server: %v", err) | ||
| default: | ||
| logrus.Errorf("Error copying from client to server: %v", err) | ||
| } | ||
| } | ||
| doneChan <- "client->server" | ||
| }() | ||
| go func() { | ||
| _, err := io.Copy(clientConn, serverConn) | ||
| if err != nil { | ||
| switch { | ||
| case errors.Is(err, io.EOF), | ||
| errors.Is(err, net.ErrClosed), | ||
| errors.Is(err, syscall.EPIPE): | ||
| logrus.Debugf("Connection closed while copying from server to client: %v", err) | ||
| default: | ||
| logrus.Errorf("Error copying from server to client: %v", err) | ||
| } | ||
| } | ||
| doneChan <- "server->client" | ||
| }() | ||
|
|
||
| // Wait for either copy to finish or context cancellation | ||
| select { | ||
| case direction := <-doneChan: | ||
| logrus.Infof("Connection closed by '%s'", direction) | ||
| case <-ctx.Done(): | ||
| logrus.Info("Context cancelled") | ||
| } | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return nil | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| *.crt | ||
| *.key |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // This module contains self-signed TLS certificates for use by the RCP | ||
| // client and proxy for mutual TLS authentication during testing. | ||
frhuelsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| package tlscerts | ||
|
|
||
| import ( | ||
| "crypto/tls" | ||
| _ "embed" | ||
| ) | ||
|
|
||
| //go:generate go run generate.go generate --san reverseconnectproxy | ||
|
|
||
| // ServerSubjectAltName is the default SAN for the server certificate. | ||
| const ServerSubjectAltName = "reverseconnectproxy" | ||
|
|
||
| type CertProvider interface { | ||
| LocalCert() (tls.Certificate, error) | ||
| RemoteCertPEM() []byte | ||
| } | ||
|
|
||
| //go:embed server.crt | ||
| var serverCert []byte | ||
|
|
||
| //go:embed client.crt | ||
| var clientCert []byte | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.