diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..de336d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +go-sql-proxy is a MySQL proxy server that acts as an intermediary between MySQL clients and servers. It provides transparent traffic forwarding with protocol decoding capabilities, metrics collection, health checks, and connection management. + +## Development Commands + +### Build and Run +```bash +# Run locally +go run main.go + +# Build binary +go build -o go-sql-proxy + +# Build Docker image +make build TAG=1.0.0 + +# Push to registry +make push TAG=1.0.0 + +# Build and push +make all TAG=1.0.0 +``` + +### Testing +Currently, there are no unit tests in the codebase. When adding new functionality, consider creating appropriate test files. + +## Architecture + +### Core Components + +1. **main.go**: Entry point that initializes configuration, starts metrics server, creates proxy instance, and handles graceful shutdown +2. **pkg/proxy**: Core proxy logic - accepts client connections, establishes upstream connections, and manages bidirectional data transfer +3. **pkg/protocol**: MySQL protocol decoding/encoding for packet inspection +4. **pkg/metrics**: Prometheus metrics collection and HTTP endpoints +5. **pkg/health**: Health check endpoints that verify proxy and upstream connectivity +6. **pkg/config**: Environment-based configuration management + +### Connection Flow + +1. Client connects to proxy on `BIND_PORT` (default: 3306) +2. Proxy establishes connection to `SOURCE_DATABASE_SERVER:SOURCE_DATABASE_PORT` +3. Data is transferred bidirectionally using `io.Copy` with optional protocol decoding +4. Metrics are collected for bytes transferred and connection counts + +### Key Design Patterns + +- **Context-based lifecycle management**: Uses Go contexts for graceful shutdown +- **Concurrent connection handling**: Each client connection runs in its own goroutine +- **Centralized logging**: All packages use the shared Logrus logger from pkg/logging +- **Environment configuration**: All settings come from environment variables + +### Environment Variables + +- `DEBUG`: Enable debug logging +- `METRICS_PORT`: Port for metrics/health endpoints (default: 9090) +- `SOURCE_DATABASE_SERVER`: Target MySQL server hostname +- `SOURCE_DATABASE_PORT`: Target MySQL server port (default: 25060) +- `SOURCE_DATABASE_USER`: MySQL username +- `SOURCE_DATABASE_PASSWORD`: MySQL password +- `SOURCE_DATABASE_NAME`: Default database name +- `BIND_ADDRESS`: Proxy bind address (default: 0.0.0.0) +- `BIND_PORT`: Proxy listening port (default: 3306) +- `USE_SSL`: Enable SSL/TLS connection to upstream MySQL (default: false) +- `SSL_SKIP_VERIFY`: Skip SSL certificate verification (default: false) +- `SSL_CA_FILE`: Path to CA certificate file for SSL verification +- `SSL_CERT_FILE`: Path to client certificate file for mutual TLS +- `SSL_KEY_FILE`: Path to client key file for mutual TLS + +### Metrics and Health Endpoints + +Available on `METRICS_PORT`: +- `/metrics`: Prometheus metrics +- `/healthz`: Liveness check (tests proxy connectivity) +- `/readyz`: Readiness check (tests upstream MySQL connectivity) +- `/version`: Version information + +### Important Implementation Details + +- The proxy uses `io.Copy` for efficient data transfer between connections +- Protocol decoding is optional and controlled by configuration +- Connection errors are logged but don't crash the proxy +- Each connection tracks bytes transferred in both directions +- Version information is injected at build time using LDFLAGS +- SSL/TLS support is controlled by the `USE_SSL` flag instead of port-based logic +- Health checks also respect SSL settings when connecting to the database \ No newline at end of file diff --git a/README.md b/README.md index 69c7a72..1fa69a7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,43 @@ To run the server, execute the following command: go run main.go ``` -You can customize the server configurations by setting the environment variables as defined in `pkg/config/config.go`. +## Configuration + +The proxy is configured through environment variables: + +### Basic Configuration +- `DEBUG`: Enable debug logging (default: false) +- `METRICS_PORT`: Port for metrics/health endpoints (default: 9090) +- `BIND_ADDRESS`: Proxy bind address (default: 0.0.0.0) +- `BIND_PORT`: Proxy listening port (default: 3306) + +### Database Connection +- `SOURCE_DATABASE_SERVER`: Target MySQL server hostname +- `SOURCE_DATABASE_PORT`: Target MySQL server port (default: 25060) +- `SOURCE_DATABASE_USER`: MySQL username +- `SOURCE_DATABASE_PASSWORD`: MySQL password +- `SOURCE_DATABASE_NAME`: Default database name + +### SSL/TLS Configuration +- `USE_SSL`: Enable SSL/TLS connection to upstream MySQL (default: false) +- `SSL_SKIP_VERIFY`: Skip SSL certificate verification (default: false) +- `SSL_CA_FILE`: Path to CA certificate file for SSL verification +- `SSL_CERT_FILE`: Path to client certificate file for mutual TLS +- `SSL_KEY_FILE`: Path to client key file for mutual TLS + +### Example: Connecting to PlanetScale + +```bash +export SOURCE_DATABASE_SERVER=your-database.planetscale.com +export SOURCE_DATABASE_PORT=3306 +export SOURCE_DATABASE_USER=your-username +export SOURCE_DATABASE_PASSWORD=your-password +export SOURCE_DATABASE_NAME=your-database +export USE_SSL=true +export SSL_SKIP_VERIFY=true + +go run main.go +``` ## Note diff --git a/charts/go-sql-proxy/Chart.yaml b/charts/go-sql-proxy/Chart.yaml index ba9c61e..4abda24 100644 --- a/charts/go-sql-proxy/Chart.yaml +++ b/charts/go-sql-proxy/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/go-sql-proxy/README.md b/charts/go-sql-proxy/README.md new file mode 100644 index 0000000..8af5e8f --- /dev/null +++ b/charts/go-sql-proxy/README.md @@ -0,0 +1,73 @@ +# go-sql-proxy Helm Chart + +This Helm chart deploys the go-sql-proxy MySQL proxy server on Kubernetes. + +## Installation + +```bash +helm install my-proxy ./charts/go-sql-proxy +``` + +## Configuration + +The following table lists the configurable parameters of the go-sql-proxy chart and their default values. + +| Parameter | Description | Default | +| --------- | ----------- | ------- | +| `settings.bind.host` | Bind address for proxy | `0.0.0.0` | +| `settings.bind.port` | Bind port for proxy | `3306` | +| `settings.debug` | Enable debug logging | `false` | +| `settings.metrics.enabled` | Enable metrics endpoint | `true` | +| `settings.metrics.port` | Metrics port | `9090` | +| `settings.source.host` | Target MySQL server hostname | `example.db.ondigitalocean.com` | +| `settings.source.port` | Target MySQL server port | `25060` | +| `settings.source.user` | MySQL username | `doadmin` | +| `settings.source.password` | MySQL password | `password` | +| `settings.source.database` | Default database name | `defaultdb` | +| `settings.ssl.enabled` | Enable SSL/TLS connection | `false` | +| `settings.ssl.skipVerify` | Skip SSL certificate verification | `false` | +| `settings.ssl.caFile` | Path to CA certificate file | `""` | +| `settings.ssl.certFile` | Path to client certificate file | `""` | +| `settings.ssl.keyFile` | Path to client key file | `""` | + +## SSL/TLS Configuration + +To connect to SSL-enabled MySQL servers (like PlanetScale), enable SSL: + +```yaml +settings: + source: + host: your-database.planetscale.com + port: 3306 + ssl: + enabled: true + skipVerify: true # For self-signed certificates +``` + +For proper certificate verification, provide CA certificate: + +```yaml +settings: + ssl: + enabled: true + skipVerify: false + caFile: /path/to/ca.pem +``` + +For mutual TLS authentication: + +```yaml +settings: + ssl: + enabled: true + certFile: /path/to/client-cert.pem + keyFile: /path/to/client-key.pem +``` + +## Monitoring + +The proxy exposes Prometheus metrics on the configured metrics port: +- `/metrics` - Prometheus metrics +- `/healthz` - Liveness probe +- `/readyz` - Readiness probe +- `/version` - Version information \ No newline at end of file diff --git a/charts/go-sql-proxy/questions.yaml b/charts/go-sql-proxy/questions.yaml index 26221c5..503035f 100644 --- a/charts/go-sql-proxy/questions.yaml +++ b/charts/go-sql-proxy/questions.yaml @@ -59,3 +59,37 @@ questions: label: "Metrics Port" type: int group: "Metrics settings" + - variable: settings.ssl.enabled + default: false + description: "Enable SSL/TLS connection to upstream MySQL server" + label: "Enable SSL" + type: bool + group: "SSL/TLS settings" + - variable: settings.ssl.skipVerify + default: false + description: "Skip SSL certificate verification (use for self-signed certificates)" + label: "Skip SSL Verify" + type: bool + group: "SSL/TLS settings" + show_if: "settings.ssl.enabled=true" + - variable: settings.ssl.caFile + default: "" + description: "Path to CA certificate file for SSL verification (optional)" + label: "CA Certificate File" + type: string + group: "SSL/TLS settings" + show_if: "settings.ssl.enabled=true" + - variable: settings.ssl.certFile + default: "" + description: "Path to client certificate file for mutual TLS (optional)" + label: "Client Certificate File" + type: string + group: "SSL/TLS settings" + show_if: "settings.ssl.enabled=true" + - variable: settings.ssl.keyFile + default: "" + description: "Path to client key file for mutual TLS (optional)" + label: "Client Key File" + type: string + group: "SSL/TLS settings" + show_if: "settings.ssl.enabled=true" diff --git a/charts/go-sql-proxy/templates/deployment.yaml b/charts/go-sql-proxy/templates/deployment.yaml index b5c5c19..daf9fcf 100644 --- a/charts/go-sql-proxy/templates/deployment.yaml +++ b/charts/go-sql-proxy/templates/deployment.yaml @@ -84,6 +84,22 @@ spec: value: "{{ .Values.settings.bind.port }}" - name: METRICS_PORT value: "{{ .Values.settings.metrics.port }}" + - name: USE_SSL + value: "{{ .Values.settings.ssl.enabled }}" + - name: SSL_SKIP_VERIFY + value: "{{ .Values.settings.ssl.skipVerify }}" + {{- if .Values.settings.ssl.caFile }} + - name: SSL_CA_FILE + value: "{{ .Values.settings.ssl.caFile }}" + {{- end }} + {{- if .Values.settings.ssl.certFile }} + - name: SSL_CERT_FILE + value: "{{ .Values.settings.ssl.certFile }}" + {{- end }} + {{- if .Values.settings.ssl.keyFile }} + - name: SSL_KEY_FILE + value: "{{ .Values.settings.ssl.keyFile }}" + {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.volumeMounts }} diff --git a/charts/go-sql-proxy/values.yaml b/charts/go-sql-proxy/values.yaml index 5a8131d..6b11153 100644 --- a/charts/go-sql-proxy/values.yaml +++ b/charts/go-sql-proxy/values.yaml @@ -16,6 +16,12 @@ settings: user: "doadmin" password: "password" database: "defaultdb" + ssl: + enabled: false + skipVerify: false + caFile: "" + certFile: "" + keyFile: "" replicaCount: 1 diff --git a/main.go b/main.go index 52f5f97..2adfcdd 100644 --- a/main.go +++ b/main.go @@ -41,16 +41,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Ensure cancel is called to release resources if main exits before signal - var useSSL bool - if config.CFG.SourceDatabasePort == 3306 { - logger.Println("SourceDatabasePort is 3306, disabling SSL") - useSSL = false - } else { - logger.Println("Enabling SSL for non-default port") - useSSL = true - } - - p := proxy.NewProxy(ctx, config.CFG.SourceDatabaseServer, config.CFG.SourceDatabasePort, useSSL) + p := proxy.NewProxy(ctx, config.CFG.SourceDatabaseServer, config.CFG.SourceDatabasePort, config.CFG.UseSSL) p.EnableDecoding = true var wg sync.WaitGroup diff --git a/pkg/config/config.go b/pkg/config/config.go index 6ea0032..549d39e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,6 +17,11 @@ type AppConfig struct { SourceDatabaseName string `json:"sourceDatabaseName"` BindAddress string `json:"bindAddress"` BindPort int `json:"bindPort"` + UseSSL bool `json:"useSSL"` + SSLSkipVerify bool `json:"sslSkipVerify"` + SSLCAFile string `json:"sslCAFile"` + SSLCertFile string `json:"sslCertFile"` + SSLKeyFile string `json:"sslKeyFile"` } // CFG is the global configuration object. @@ -33,6 +38,11 @@ func LoadConfiguration() { CFG.SourceDatabaseName = getEnvOrDefault("SOURCE_DATABASE_NAME", "defaultdb") CFG.BindAddress = getEnvOrDefault("BIND_ADDRESS", "0.0.0.0") CFG.BindPort = parseEnvInt("BIND_PORT", 3306) + CFG.UseSSL = parseEnvBool("USE_SSL", false) + CFG.SSLSkipVerify = parseEnvBool("SSL_SKIP_VERIFY", false) + CFG.SSLCAFile = getEnvOrDefault("SSL_CA_FILE", "") + CFG.SSLCertFile = getEnvOrDefault("SSL_CERT_FILE", "") + CFG.SSLKeyFile = getEnvOrDefault("SSL_KEY_FILE", "") } func getEnvOrDefault(key, defaultValue string) string { diff --git a/pkg/health/health.go b/pkg/health/health.go index d6c5046..2300033 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -8,6 +8,7 @@ import ( // Import MySQL driver for database connectivity _ "github.com/go-sql-driver/mysql" + "github.com/supporttools/go-sql-proxy/pkg/config" "github.com/supporttools/go-sql-proxy/pkg/logging" ) @@ -35,7 +36,7 @@ func HealthzHandler(username, password, host string, port int, database string) logger.Info("HealthzHandler") // Construct the DSN (Data Source Name) string - dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", username, password, host, port, database) + dsn := buildDSN(username, password, host, port, database) // Open a new database connection conn, err := sql.Open("mysql", dsn) @@ -65,7 +66,7 @@ func ReadyzHandler(username, password, host string, port int, database string) h logger.Info("ReadyzHandler") // Construct the DSN (Data Source Name) string - dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", username, password, host, port, database) + dsn := buildDSN(username, password, host, port, database) // Open a new database connection conn, err := sql.Open("mysql", dsn) @@ -107,3 +108,23 @@ func VersionHandler() http.HandlerFunc { } } } + +// buildDSN constructs a MySQL DSN with optional TLS parameters +func buildDSN(username, password, host string, port int, database string) string { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", username, password, host, port, database) + + if config.CFG.UseSSL { + // Add TLS parameters to the DSN + tlsConfig := "?tls=true" + + // For custom CA verification, we'd need to register a custom TLS config + // with the MySQL driver, but for basic SSL with skip-verify, this works + if config.CFG.SSLSkipVerify { + tlsConfig = "?tls=skip-verify" + } + + dsn += tlsConfig + } + + return dsn +} diff --git a/pkg/proxy/HandleConnection.go b/pkg/proxy/HandleConnection.go index 30c5f1d..ba0aa03 100644 --- a/pkg/proxy/HandleConnection.go +++ b/pkg/proxy/HandleConnection.go @@ -1,10 +1,14 @@ package proxy import ( + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "log" "net" + "github.com/supporttools/go-sql-proxy/pkg/config" "github.com/supporttools/go-sql-proxy/pkg/metrics" "github.com/supporttools/go-sql-proxy/pkg/models" ) @@ -12,7 +16,16 @@ import ( // HandleConnection starts the proxy connection, handling data transfer and optional protocol decoding. func HandleConnection(c *models.Connection) error { address := fmt.Sprintf("%s:%d", c.Host, c.Port) - mysqlConn, err := net.Dial("tcp", address) + + var mysqlConn net.Conn + var err error + + if config.CFG.UseSSL { + mysqlConn, err = dialWithSSL(address) + } else { + mysqlConn, err = net.Dial("tcp", address) + } + if err != nil { log.Printf("Failed to connect to MySQL [%d]: %s", c.ID, err) return err @@ -35,3 +48,35 @@ func HandleConnection(c *models.Connection) error { return handleProtocolDecoding(c, mysqlConn) } + +// dialWithSSL creates an SSL/TLS connection to the MySQL server +func dialWithSSL(address string) (net.Conn, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: config.CFG.SSLSkipVerify, + } + + // Load custom CA if provided + if config.CFG.SSLCAFile != "" { + caCert, err := ioutil.ReadFile(config.CFG.SSLCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read CA file: %w", err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse CA certificate") + } + tlsConfig.RootCAs = caCertPool + } + + // Load client certificates if provided + if config.CFG.SSLCertFile != "" && config.CFG.SSLKeyFile != "" { + cert, err := tls.LoadX509KeyPair(config.CFG.SSLCertFile, config.CFG.SSLKeyFile) + if err != nil { + return nil, fmt.Errorf("failed to load client certificates: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + return tls.Dial("tcp", address, tlsConfig) +}