feat(ssl): Add LetsEncrypt certificate option

This commit is contained in:
Björn Benouarets
2025-12-16 14:15:16 +01:00
parent 69a42d957d
commit eec632ff97
10 changed files with 568 additions and 11 deletions

View File

@@ -1,6 +1,7 @@
package proxy
import (
"crypto/tls"
"fmt"
"io"
"net"
@@ -53,11 +54,65 @@ func (p *Proxy) handleConnection(clientConn net.Conn) {
remoteAddr := clientConn.RemoteAddr().String()
masterlog.Info("New connection", map[string]interface{}{"remoteAddr": remoteAddr})
// Create peek connection to inspect first bytes
peekConn := newPeekConn(clientConn)
var hostname string
var peekConn *peekConn
// Try to extract hostname
hostname := p.extractHostname(peekConn)
// Create peek connection to inspect first bytes
tempPeekConn := newPeekConn(clientConn)
// Check if this is a TLS handshake (first byte is 0x16)
firstByte, err := tempPeekConn.peek(1)
if err != nil {
masterlog.Error("Failed to peek connection", map[string]interface{}{"error": err, "remoteAddr": remoteAddr})
return
}
// If TLS is enabled and this looks like a TLS handshake, upgrade to TLS
if p.config.TLS.Enabled && len(firstByte) > 0 && firstByte[0] == 0x16 {
// Get TLS config
var tlsConfig *tls.Config
if p.certmagic != nil {
// Use Let's Encrypt certmagic
tlsConfig = createTLSConfigWithLetsEncrypt(p.certmagic)
} else if p.certificate != nil {
// Use stored certificate for regular TLS
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{*p.certificate},
ClientAuth: tls.NoClientCert,
}
} else {
// Fallback: get certificate on demand
cert, err := getCertificateForMappings(p.config)
if err != nil {
masterlog.Error("Failed to get TLS certificate", map[string]interface{}{"error": err})
return
}
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
}
}
// Upgrade connection to TLS
tlsConn := tls.Server(clientConn, tlsConfig)
if err := tlsConn.Handshake(); err != nil {
masterlog.Error("TLS handshake failed", map[string]interface{}{"error": err, "remoteAddr": remoteAddr})
return
}
// Extract SNI from connection state
state := tlsConn.ConnectionState()
if state.ServerName != "" {
hostname = state.ServerName
masterlog.Info("Extracted hostname from TLS SNI", map[string]interface{}{"hostname": hostname})
}
peekConn = newPeekConn(tlsConn)
} else {
// Non-TLS connection
peekConn = tempPeekConn
// Try to extract hostname from raw connection (won't work for non-TLS, but that's OK)
hostname = p.extractHostname(peekConn)
}
var (
targetMapping struct {
host string
@@ -94,6 +149,16 @@ func (p *Proxy) handleConnection(clientConn net.Conn) {
masterlog.Info("Proxying", map[string]interface{}{"hostname": hostname, "host": targetMapping.host, "port": targetMapping.port})
// If TLS is enabled, handle PostgreSQL SSL request
// After TLS handshake, PostgreSQL clients may send an SSL request
// We respond with 'S' (SSL supported) since encryption is already handled by TLS
if p.config.TLS.Enabled {
if err := p.handlePostgresSSLRequest(peekConn); err != nil {
masterlog.Error("Failed to handle PostgreSQL SSL request", map[string]interface{}{"error": err})
return
}
}
// Connect to backend PostgreSQL server
backendAddr := net.JoinHostPort(targetMapping.host, fmt.Sprintf("%d", targetMapping.port))
backendConn, err := net.DialTimeout("tcp", backendAddr, 10*time.Second)
@@ -339,3 +404,48 @@ func (p *Proxy) extractHostnameFromPostgresStartup(conn *peekConn) string {
return ""
}
// handlePostgresSSLRequest handles PostgreSQL SSL request after TLS handshake
// PostgreSQL clients may send an SSL request (0x00 0x00 0x00 0x08 0x04 0xD2 0x16 0x2F)
// We respond with 'S' (0x53) to indicate SSL is supported, since encryption is already handled by TLS
func (p *Proxy) handlePostgresSSLRequest(conn *peekConn) error {
// Peek at first 8 bytes to check for SSL request
peekBuf, err := conn.peek(8)
if err != nil {
// If we can't peek, it's not an SSL request, continue normally
return nil
}
// PostgreSQL SSL request: 4 bytes length (0x00 0x00 0x00 0x08) + 4 bytes code (0x04 0xD2 0x16 0x2F)
sslRequest := []byte{0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F}
if len(peekBuf) >= 8 {
isSSLRequest := true
for i := 0; i < 8; i++ {
if peekBuf[i] != sslRequest[i] {
isSSLRequest = false
break
}
}
if isSSLRequest {
// Consume the SSL request (read it from the connection)
buf := make([]byte, 8)
_, err := io.ReadFull(conn, buf)
if err != nil {
return fmt.Errorf("failed to read SSL request: %w", err)
}
// Respond with 'S' (0x53) - SSL supported
// Since encryption is already handled by TLS termination, we signal SSL support
_, err = conn.Write([]byte{'S'})
if err != nil {
return fmt.Errorf("failed to write SSL response: %w", err)
}
masterlog.Info("Handled PostgreSQL SSL request", map[string]interface{}{"response": "S", "reason": "TLS already enabled"})
}
}
return nil
}