Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
},
"protobuf.externalLinter.enabled": true,
"protobuf.externalLinter.linter": "buf",
"go.buildTags": "tls_server,tls_client",
}
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,22 @@ bin/virtdeploy: tools/cmd/virtdeploy/* tools/go.sum tools/pkg/* tools/pkg/virtde
@mkdir -p bin
cd tools && go build -o ../bin/virtdeploy ./cmd/virtdeploy

bin/rcp-proxy: FORCE
@mkdir -p bin
cd tools && go generate pkg/rcp/tlscerts/certs.go
cd tools && go build -tags tls_client -o ../bin/rcp-proxy ./cmd/rcp-proxy/main.go

# Clean generated RCP TLS certificates
.PHONY: clean-rcp-certs
clean-rcp-certs:
cd tools/pkg/rcp/tlscerts && go run generate.go clean

# An empty target to force rebuilds of anything that depends on it. Useful for
# tools that are smarter than Make and only rebuild when source files change.
# (eg. go build)
.PHONY: FORCE
FORCE:

# Installer tools

INSTALLER_OUT_DIR := bin
Expand Down
43 changes: 43 additions & 0 deletions tools/cmd/rcp-proxy/main.go
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)
if err := proxy.StartReverseConnectProxy(ctx, tlscerts.ClientCertProvider, cli.ClientAddress, cli.ServerAddress, time.Second); err != nil {
logrus.Fatalf("reverse-connect proxy error: %v", err)
}
logrus.Info("Shutdown complete")
}
82 changes: 82 additions & 0 deletions tools/pkg/rcp/client/listen.go
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)
}

// 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")
}

// 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()

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()
}()

// 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
}
5 changes: 5 additions & 0 deletions tools/pkg/rcp/harpoon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package rcp

const (
DefaultTridentSocketPath = "/run/trident.sock"
)
164 changes: 164 additions & 0 deletions tools/pkg/rcp/proxy/proxy.go
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.
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,
MinVersion: tls.VersionTLS13,
}

// Bool to keep track if the connection has been refused multiple times
// consecutively, to reduce log spam.
multipleRefused := false

// 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
}

// 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()

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")
}

return nil
}
2 changes: 2 additions & 0 deletions tools/pkg/rcp/tlscerts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.crt
*.key
25 changes: 25 additions & 0 deletions tools/pkg/rcp/tlscerts/certs.go
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.

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
Loading