diff options
| -rw-r--r-- | cmd/mobius-hotline-server/main.go | 26 | ||||
| -rw-r--r-- | docs/tls.md | 101 | ||||
| -rw-r--r-- | hotline/server.go | 47 |
3 files changed, 171 insertions, 3 deletions
diff --git a/cmd/mobius-hotline-server/main.go b/cmd/mobius-hotline-server/main.go index aa41279..cc24f5f 100644 --- a/cmd/mobius-hotline-server/main.go +++ b/cmd/mobius-hotline-server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/tls" "embed" "flag" "fmt" @@ -44,6 +45,9 @@ func main() { logLevel := flag.String("log-level", "info", "Log level") logFile := flag.String("log-file", "", "Path to log file") init := flag.Bool("init", false, "Populate the config dir with default configuration") + tlsCert := flag.String("tls-cert", "", "Path to TLS certificate file") + tlsKey := flag.String("tls-key", "", "Path to TLS key file") + tlsPort := flag.Int("tls-port", 5600, "Base TLS port. TLS file transfer port is base + 1.") flag.Parse() @@ -78,12 +82,27 @@ func main() { os.Exit(1) } - srv, err := hotline.NewServer( + var tlsConfig *tls.Config + if *tlsCert != "" && *tlsKey != "" { + cert, err := tls.LoadX509KeyPair(*tlsCert, *tlsKey) + if err != nil { + slogger.Error("Error loading TLS certificate", "err", err) + os.Exit(1) + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + opts := []hotline.Option{ hotline.WithInterface(*netInterface), hotline.WithLogger(slogger), hotline.WithPort(*basePort), hotline.WithConfig(*config), - ) + } + if tlsConfig != nil { + opts = append(opts, hotline.WithTLS(tlsConfig, *tlsPort)) + } + + srv, err := hotline.NewServer(opts...) if err != nil { slogger.Error("Error starting server", "err", err) os.Exit(1) @@ -174,6 +193,9 @@ func main() { }() slogger.Info("Hotline server started", "version", version, "config", *configDir) + if tlsConfig != nil { + slogger.Info("TLS enabled", "port", *tlsPort, "fileTransferPort", *tlsPort+1) + } // Assign functions to handle specific Hotline transaction types mobius.RegisterHandlers(srv) diff --git a/docs/tls.md b/docs/tls.md new file mode 100644 index 0000000..6cb2fd5 --- /dev/null +++ b/docs/tls.md @@ -0,0 +1,101 @@ +# TLS Support + +Mobius supports TLS (Transport Layer Security) for encrypted connections between clients and the server. When enabled, TLS runs on separate ports alongside the standard unencrypted ports, allowing both secure and legacy client connections simultaneously. + +## Ports + +| Service | Standard Port | TLS Port (default) | +|---------------|---------------|-------------------| +| Hotline | 5500 | 5600 | +| File Transfer | 5501 | 5601 | + +## Generating Certificates + +### Self-Signed Certificate (Testing/Private Use) + +For testing or private servers, you can generate a self-signed certificate using OpenSSL: + +```bash +openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj "/CN=localhost" +``` + +This creates: +- `server.key` - Private key file +- `server.crt` - Certificate file + +For a certificate that includes your server's hostname or IP address: + +```bash +openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \ + -subj "/CN=your-hostname.example.com" \ + -addext "subjectAltName=DNS:your-hostname.example.com,IP:192.168.1.100" +``` + +### Let's Encrypt (Production) + +For production servers with a public domain name, use [Let's Encrypt](https://letsencrypt.org/) with certbot: + +```bash +certbot certonly --standalone -d your-hostname.example.com +``` + +The certificates are typically stored at: +- `/etc/letsencrypt/live/your-hostname.example.com/fullchain.pem` +- `/etc/letsencrypt/live/your-hostname.example.com/privkey.pem` + +## Command-Line Options + +| Flag | Description | Default | +|------|-------------|---------| +| `-tls-cert` | Path to TLS certificate file | (none) | +| `-tls-key` | Path to TLS private key file | (none) | +| `-tls-port` | Base TLS port (file transfer uses base + 1) | 5600 | + +TLS is enabled when both `-tls-cert` and `-tls-key` are provided. + +## Usage Examples + +### Basic TLS Setup + +```bash +mobius-hotline-server -tls-cert server.crt -tls-key server.key +``` + +### Custom TLS Port + +```bash +mobius-hotline-server -tls-cert server.crt -tls-key server.key -tls-port 5700 +``` + +### Full Example with All Options + +```bash +mobius-hotline-server \ + -config /path/to/config \ + -bind 5500 \ + -tls-cert /etc/letsencrypt/live/example.com/fullchain.pem \ + -tls-key /etc/letsencrypt/live/example.com/privkey.pem \ + -tls-port 5600 +``` + +## Verifying TLS is Working + +When TLS is enabled, you'll see a log message at startup: + +``` +TLS enabled port=5600 fileTransferPort=5601 +``` + +You can verify the TLS connection using OpenSSL: + +```bash +openssl s_client -connect localhost:5600 +``` + +## Client Configuration + +Clients connecting via TLS must: +1. Connect to the TLS port (default 5600) instead of the standard port (5500) +2. Support TLS connections (client-dependent) + +Note: Self-signed certificates may require clients to accept or trust the certificate manually. diff --git a/hotline/server.go b/hotline/server.go index 0395d7b..2c25e93 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "crypto/rand" + "crypto/tls" "encoding/binary" "errors" "fmt" @@ -40,7 +41,8 @@ type Server struct { NetInterface string Port int - rateLimiters map[string]*rate.Limiter + rateLimiters map[string]*rate.Limiter + rateLimitersMu sync.Mutex handlers map[TranType]HandlerFunc @@ -71,6 +73,9 @@ type Server struct { // TrackerRegistrar handles tracker registration (injectable for testing) TrackerRegistrar TrackerRegistrar + + TLSConfig *tls.Config + TLSPort int } type Option = func(s *Server) @@ -108,6 +113,14 @@ func WithTrackerRegistrar(registrar TrackerRegistrar) func(s *Server) { } } +// WithTLS optionally enables TLS support on the specified port. +func WithTLS(tlsConfig *tls.Config, port int) func(s *Server) { + return func(s *Server) { + s.TLSConfig = tlsConfig + s.TLSPort = port + } +} + type ServerConfig struct { } @@ -168,6 +181,28 @@ func (s *Server) ListenAndServe(ctx context.Context) error { log.Fatal(s.ServeFileTransfers(ctx, ln)) }() + if s.TLSConfig != nil { + wg.Add(1) + go func() { + ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.TLSPort)) + if err != nil { + log.Fatal(err) + } + + log.Fatal(s.ServeWithTLS(ctx, ln)) + }() + + wg.Add(1) + go func() { + ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.TLSPort+1)) + if err != nil { + log.Fatal(err) + } + + log.Fatal(s.ServeFileTransfersWithTLS(ctx, ln)) + }() + } + wg.Wait() return nil @@ -195,6 +230,14 @@ func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error } } +func (s *Server) ServeWithTLS(ctx context.Context, ln net.Listener) error { + return s.Serve(ctx, tls.NewListener(ln, s.TLSConfig)) +} + +func (s *Server) ServeFileTransfersWithTLS(ctx context.Context, ln net.Listener) error { + return s.ServeFileTransfers(ctx, tls.NewListener(ln, s.TLSConfig)) +} + func (s *Server) sendTransaction(t Transaction) error { client := s.ClientMgr.Get(t.ClientID) @@ -249,11 +292,13 @@ func (s *Server) Serve(ctx context.Context, ln net.Listener) error { defer func() { _ = conn.Close() }() // Check if we have an existing rate limit for the IP and create one if we do not. + s.rateLimitersMu.Lock() rl, ok := s.rateLimiters[ipAddr] if !ok { rl = rate.NewLimiter(perIPRateLimit, 1) s.rateLimiters[ipAddr] = rl } + s.rateLimitersMu.Unlock() // Check if the rate limit is exceeded and close the connection if so. if !rl.Allow() { |