diff --git a/README.md b/README.md index 1e22a34..4531ae2 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Both services share the same encrypted tunnel connection! ## Features -- **Secure Encryption**: All traffic is encrypted using AES-GCM encryption with PBKDF2 key derivation +- **Secure Encryption**: All traffic is encrypted using AES-GCM. New keys are raw 256-bit AES keys; passphrase-style configs still use PBKDF2 - **Port Forwarding**: Forward multiple ports with different protocols (TCP and UDP) - **Configuration-based**: Easy configuration via YAML files - **Bidirectional**: Full bidirectional port forwarding @@ -186,8 +186,8 @@ dns_server: - `"tcp://80:8080"` (client) - listen on `127.0.0.1:8080`, forward to teleport server's port 80 - `"tcp://22:0.0.0.0:2222"` (client) - listen on all interfaces port 2222 - `"udp://53:5353"` (client) - listen on `127.0.0.1:5353`, forward to teleport server's port 53 -- `encryption_key`: Shared secret key for encryption (must be the same on both sides) -- `keep_alive`: Enable TCP keep-alive +- `encryption_key`: Shared secret (must be the same on both sides). New keys from `--generate-key` / `--generate-config` are `raw:` plus 64 hex characters (32-byte AES-256 key, no PBKDF2). Unprefixed values — passphrases and hex strings from older `--generate-key` — still go through PBKDF2 so existing configs keep working. To migrate an old hex key to raw AES, re-generate with `--generate-key` and update both sides together; do not strip `raw:` from a new key or add `raw:` to an old hex string. +- `keep_alive`: Enable TCP keep-alive on tunnel and forwarded TCP connections (helps long-lived forwards through NAT) - `read_timeout`: Read timeout duration - `write_timeout`: Write timeout duration - `max_connections`: Maximum concurrent connections (default: 1000 for server, 100 for client) @@ -265,7 +265,9 @@ dns_server: ./teleport -k ``` -This generates a cryptographically secure 256-bit encryption key that you can use in your configuration files. +This prints a `raw:` prefixed 256-bit key. Paste the entire value into `encryption_key` on both server and client. It is used as an AES-256 key directly (no PBKDF2). Existing configs that store a passphrase or an unprefixed hex string still use PBKDF2. + +`--generate-config` writes the file mode `0600` because the file embeds a live encryption key. ### Logging Options @@ -503,6 +505,8 @@ rate_limit: window_size: 1s # Time window for rate limiting ``` +The token bucket is process-global (not per source IP). One noisy peer can consume the budget for everyone; that is enough for a single-user homelab. + ### Advanced Logging @@ -523,7 +527,7 @@ Teleport includes sophisticated logging with: ## Notes - The encryption key must be identical on both server and client -- Use `./teleport --generate-key` to create a secure random encryption key +- Use `./teleport --generate-key` to create a raw 256-bit AES key (`raw:` + 64 hex). Unprefixed keys in existing configs still use PBKDF2 - The server listens on the specified `listen_address` for incoming tunnel connections (examples bind loopback; `0.0.0.0` must be explicit) - The client connects to the remote server and forwards local connections from `bind_address` (default `127.0.0.1`) - All port forwarding is bidirectional diff --git a/cmd/teleport/main.go b/cmd/teleport/main.go index f1bad6f..31c09db 100644 --- a/cmd/teleport/main.go +++ b/cmd/teleport/main.go @@ -1,8 +1,6 @@ package main import ( - "crypto/rand" - "encoding/hex" "fmt" "os" "os/signal" @@ -62,7 +60,8 @@ func main() { return } fmt.Printf("Generated encryption key: %s\n", key) - fmt.Println("Use this key in your configuration file for both server and client.") + fmt.Println("Paste the entire value into encryption_key on both server and client.") + fmt.Println("This is a raw 256-bit AES key (no PBKDF2). Unprefixed keys in existing configs still use PBKDF2.") return } @@ -116,21 +115,14 @@ func main() { } } -// generateRandomKey generates a cryptographically secure random encryption key +// generateRandomKey generates a raw 256-bit AES key (raw: + 64 hex chars). func generateRandomKey() (string, error) { - // Generate 32 random bytes (256 bits) for a strong encryption key - bytes := make([]byte, 32) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("failed to generate random key: %v", err) + key, err := encryption.GenerateRawKey() + if err != nil { + return "", err } - - // Convert to hexadecimal string for easy copying - key := hex.EncodeToString(bytes) - - // Validate the generated key if err := encryption.ValidateEncryptionKey(key); err != nil { return "", fmt.Errorf("generated key failed validation: %v", err) } - return key, nil } diff --git a/internal/client/client.go b/internal/client/client.go index a3d89b4..58d2c89 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -50,7 +50,7 @@ func NewTeleportClient(config *config.Config) *TeleportClient { cancel: cancel, connectionPool: make(chan net.Conn, maxPoolSize), maxPoolSize: maxPoolSize, - derivedKey: encryption.DeriveKey(config.EncryptionKey), + derivedKey: mustResolveKey(config.EncryptionKey), } } @@ -63,6 +63,7 @@ func (tc *TeleportClient) Start() error { if err != nil { return fmt.Errorf("failed to connect to server: %v", err) } + config.ApplyTCPKeepAlive(conn, tc.config.KeepAlive) tc.serverConn = conn // Start DNS server if enabled @@ -187,6 +188,7 @@ func (tc *TeleportClient) getConnection() (net.Conn, error) { if err != nil { return nil, fmt.Errorf("failed to create connection: %v", err) } + config.ApplyTCPKeepAlive(conn, tc.config.KeepAlive) // Set connection timeouts if tc.config.ReadTimeout > 0 { @@ -343,6 +345,7 @@ func (tc *TeleportClient) sendTaggedUDPPacketWithResponse(packet types.TaggedUDP return } defer serverConn.Close() + config.ApplyTCPKeepAlive(serverConn, tc.config.KeepAlive) logger.WithField("packetID", packet.Header.PacketID).Debug("UDP CLIENT: Connected to server, sending port forward request") @@ -399,9 +402,8 @@ func (tc *TeleportClient) sendTaggedUDPPacketToConnection(conn net.Conn, packet "data_length": len(data), }).Debug("UDP CLIENT: Serialized packet") - // Encrypt the data - key := encryption.DeriveKey(tc.config.EncryptionKey) - encryptedData, err := encryption.EncryptData(data, key) + // Encrypt the data (key resolved once at process start) + encryptedData, err := encryption.EncryptData(data, tc.derivedKey) if err != nil { logger.WithFields(map[string]interface{}{ "packetID": packet.Header.PacketID, @@ -453,9 +455,8 @@ func (tc *TeleportClient) waitForUDPResponseAndForward(conn net.Conn, expectedPa "bytes_received": n, }).Debug("UDP CLIENT: Received response bytes") - // Decrypt the response - key := encryption.DeriveKey(tc.config.EncryptionKey) - decryptedData, err := encryption.DecryptData(buffer[:n], key) + // Decrypt the response (key resolved once at process start) + decryptedData, err := encryption.DecryptData(buffer[:n], tc.derivedKey) if err != nil { logger.WithFields(map[string]interface{}{ "packetID": expectedPacketID, @@ -638,6 +639,7 @@ func (tc *TeleportClient) deserializeTaggedUDPPacket(data []byte) (types.TaggedU // handleTCPConnection handles a TCP connection from a local client func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.PortRule) { defer clientConn.Close() + config.ApplyTCPKeepAlive(clientConn, tc.config.KeepAlive) // Get a connection from the pool or create a new one serverConn, err := tc.getConnection() @@ -825,3 +827,12 @@ func (tc *TeleportClient) serializeRequest(request types.PortForwardRequest) ([] return data, nil } + +func mustResolveKey(material string) []byte { + key, err := encryption.ResolveKey(material) + if err != nil { + logger.WithField("error", err).Error("Failed to resolve encryption key") + return nil + } + return key +} diff --git a/internal/server/server.go b/internal/server/server.go index a5ad42f..bec3491 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -77,7 +77,7 @@ func NewTeleportServer(config *config.Config) *TeleportServer { goroutineSem: make(chan struct{}, maxGoroutines), maxGoroutines: maxGoroutines, metrics: metricsInstance, - derivedKey: encryption.DeriveKey(config.EncryptionKey), + derivedKey: mustResolveKey(config.EncryptionKey), } } @@ -254,9 +254,8 @@ func (ts *TeleportServer) sendTaggedUDPPacket(clientConn *net.UDPConn, packet ty return } - // Encrypt the data - key := encryption.DeriveKey(ts.config.EncryptionKey) - encryptedData, err := encryption.EncryptData(data, key) + // Encrypt the data (key resolved once at process start) + encryptedData, err := encryption.EncryptData(data, ts.derivedKey) if err != nil { logger.WithField("error", err).Debug("Failed to encrypt UDP packet") return @@ -310,6 +309,8 @@ func (ts *TeleportServer) handleConnectionWithLimit(conn net.Conn) { ts.metrics.DecrementActiveConnections() }() + config.ApplyTCPKeepAlive(conn, ts.config.KeepAlive) + // Set connection timeouts if ts.config.ReadTimeout > 0 { conn.SetReadDeadline(time.Now().Add(ts.config.ReadTimeout)) @@ -393,6 +394,7 @@ func (ts *TeleportServer) handleTCPForward(clientConn net.Conn, rule *config.Por return } defer targetConn.Close() + config.ApplyTCPKeepAlive(targetConn, ts.config.KeepAlive) logger.WithFields(map[string]interface{}{ "client": clientConn.RemoteAddr(), @@ -478,9 +480,8 @@ func (ts *TeleportServer) handleUDPForward(clientConn net.Conn, rule *config.Por logger.WithField("bytes_received", n).Debug("UDP SERVER: Received bytes from client") - // Decrypt the data - key := encryption.DeriveKey(ts.config.EncryptionKey) - decryptedData, err := encryption.DecryptData(buffer[:n], key) + // Decrypt the data (key resolved once at process start) + decryptedData, err := encryption.DecryptData(buffer[:n], ts.derivedKey) if err != nil { logger.WithField("error", err).Debug("UDP SERVER: Failed to decrypt UDP packet") continue @@ -595,8 +596,7 @@ func (ts *TeleportServer) handleUDPForward(clientConn net.Conn, rule *config.Por continue } - key := encryption.DeriveKey(ts.config.EncryptionKey) - encryptedData, err := encryption.EncryptData(data, key) + encryptedData, err := encryption.EncryptData(data, ts.derivedKey) if err != nil { logger.WithFields(map[string]interface{}{ "packetID": originalPacketID, @@ -897,3 +897,12 @@ func (ts *TeleportServer) deserializeRequest(data []byte, request *types.PortFor return nil } + +func mustResolveKey(material string) []byte { + key, err := encryption.ResolveKey(material) + if err != nil { + logger.WithField("error", err).Error("Failed to resolve encryption key") + return nil + } + return key +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 0d819b9..0bcc7d4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,8 +1,6 @@ package config import ( - "crypto/rand" - "encoding/hex" "fmt" "net" "os" @@ -609,24 +607,33 @@ func GenerateExampleConfig(filename string) error { return fmt.Errorf("failed to marshal config: %v", err) } - err = os.WriteFile(filename, data, 0644) + err = os.WriteFile(filename, data, 0o600) if err != nil { return fmt.Errorf("failed to write config file: %v", err) } + if err := os.Chmod(filename, 0o600); err != nil { + return fmt.Errorf("failed to set config file permissions: %v", err) + } fmt.Printf("Generated example configuration: %s\n", filename) fmt.Printf("Edit the configuration file and run: ./teleport -config %s\n", filename) return nil } -// generateStrongEncryptionKey generates a cryptographically secure encryption key +// generateStrongEncryptionKey generates a raw 256-bit key (raw: + 64 hex chars). func generateStrongEncryptionKey() (string, error) { - // Generate 32 random bytes (256 bits) for a strong encryption key - bytes := make([]byte, 32) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("failed to generate random key: %v", err) - } - - // Convert to hexadecimal string for easy copying - return hex.EncodeToString(bytes), nil + return encryption.GenerateRawKey() +} + +// ApplyTCPKeepAlive enables TCP keep-alive when enabled is true. +func ApplyTCPKeepAlive(conn net.Conn, enabled bool) { + if conn == nil || !enabled { + return + } + tcp, ok := conn.(*net.TCPConn) + if !ok { + return + } + _ = tcp.SetKeepAlive(true) + _ = tcp.SetKeepAlivePeriod(30 * time.Second) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 0f4d6f5..da26d5e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" "time" + + "teleport/pkg/encryption" ) func TestLoadConfig(t *testing.T) { @@ -585,3 +587,61 @@ func TestGenerateExampleConfigUsesLoopback(t *testing.T) { t.Fatalf("generated client local listen %q", got) } } + +func TestGenerateExampleConfigMode0600(t *testing.T) { + tempDir := t.TempDir() + configFile := filepath.Join(tempDir, "server.yaml") + if err := GenerateExampleConfig(configFile); err != nil { + t.Fatal(err) + } + st, err := os.Stat(configFile) + if err != nil { + t.Fatal(err) + } + if st.Mode().Perm() != 0o600 { + t.Fatalf("generated config mode %04o want 0600", st.Mode().Perm()) + } + cfg, err := LoadConfig(configFile) + if err != nil { + t.Fatalf("reload generated config: %v", err) + } + if !encryption.IsRawKey(cfg.EncryptionKey) { + t.Fatalf("generated encryption_key is not raw: %q", cfg.EncryptionKey[:min(8, len(cfg.EncryptionKey))]) + } + key, err := encryption.ResolveKey(cfg.EncryptionKey) + if err != nil { + t.Fatal(err) + } + if len(key) != 32 { + t.Fatalf("resolved key len %d", len(key)) + } +} + +func TestApplyTCPKeepAlive(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + errCh := make(chan error, 1) + go func() { + c, err := ln.Accept() + if err != nil { + errCh <- err + return + } + defer c.Close() + ApplyTCPKeepAlive(c, true) + errCh <- nil + }() + conn, err := net.Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + ApplyTCPKeepAlive(conn, true) + ApplyTCPKeepAlive(conn, false) + if err := <-errCh; err != nil { + t.Fatal(err) + } +} diff --git a/pkg/encryption/encryption.go b/pkg/encryption/encryption.go index 8333ed1..0b34d7d 100644 --- a/pkg/encryption/encryption.go +++ b/pkg/encryption/encryption.go @@ -6,9 +6,11 @@ import ( "crypto/rand" "crypto/sha256" "crypto/subtle" + "encoding/hex" "fmt" "io" "math" + "strings" "sync" "time" @@ -24,22 +26,69 @@ const ( // Replay protection parameters MaxPacketAge = 5 * time.Minute // Maximum age for UDP packets NonceWindow = 1000 // Number of nonces to track for replay protection + + // RawKeyPrefix marks a hex-encoded 32-byte AES key that must not go through PBKDF2. + RawKeyPrefix = "raw:" + RawKeyHexLen = 64 // 32 bytes ) -// DeriveKey derives an encryption key from a password using PBKDF2 +// deriveCache memoizes PBKDF2 so handshake/UDP paths never pay 100k iterations twice. +var deriveCache sync.Map // map[string][]byte + +// GenerateRawKey returns a raw: prefixed hex encoding of 32 random bytes. +// Paste the entire string into encryption_key; it is used as an AES-256 key (no PBKDF2). +func GenerateRawKey() (string, error) { + key := make([]byte, PBKDF2KeyLength) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return "", fmt.Errorf("failed to generate random key: %v", err) + } + return RawKeyPrefix + hex.EncodeToString(key), nil +} + +// IsRawKey reports whether material is a raw 256-bit key (raw: + 64 hex chars). +func IsRawKey(material string) bool { + return strings.HasPrefix(material, RawKeyPrefix) +} + +// ResolveKey returns a 32-byte AES key from config material. +// +// - raw:<64 hex>: hex-decode, no PBKDF2 (new --generate-key / generated configs) +// - anything else, including legacy unprefixed 64-hex from older --generate-key: +// PBKDF2 with the historical password-derived salt (existing configs keep working) +func ResolveKey(material string) ([]byte, error) { + if strings.HasPrefix(material, RawKeyPrefix) { + hexStr := material[len(RawKeyPrefix):] + key, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("invalid raw encryption key: %w", err) + } + if len(key) != PBKDF2KeyLength { + return nil, fmt.Errorf("raw encryption key must be %d bytes, got %d", PBKDF2KeyLength, len(key)) + } + return key, nil + } + return DeriveKey(material), nil +} + +// DeriveKey derives an encryption key from a password using PBKDF2. +// The first call for a given password runs 100k iterations; later calls return the cached key. func DeriveKey(password string) []byte { - // Use a deterministic salt derived from the password hash for consistent key derivation - // This ensures the same password always produces the same key while avoiding rainbow tables + if v, ok := deriveCache.Load(password); ok { + return v.([]byte) + } + + // Historical salt is SHA256(password)[:16]. That is not a random salt; keep it only + // so existing passphrase and unprefixed hex configs still derive the same key. hasher := sha256.New() hasher.Write([]byte(password)) passwordHash := hasher.Sum(nil) - // Create a deterministic salt from the password hash salt := make([]byte, PBKDF2SaltLength) copy(salt, passwordHash[:PBKDF2SaltLength]) key := pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, PBKDF2KeyLength, sha256.New) - return key + actual, _ := deriveCache.LoadOrStore(password, key) + return actual.([]byte) } // DeriveKeyWithSalt derives an encryption key from a password using PBKDF2 with a custom salt @@ -176,6 +225,21 @@ func ConstantTimeCompare(a, b []byte) bool { // ValidateEncryptionKey validates that an encryption key meets security requirements func ValidateEncryptionKey(key string) error { + if strings.HasPrefix(key, RawKeyPrefix) { + hexStr := key[len(RawKeyPrefix):] + if len(hexStr) != RawKeyHexLen { + return fmt.Errorf("raw encryption key must be %d hex characters (32 bytes), got %d", RawKeyHexLen, len(hexStr)) + } + decoded, err := hex.DecodeString(hexStr) + if err != nil { + return fmt.Errorf("raw encryption key is not valid hex: %w", err) + } + if len(decoded) != PBKDF2KeyLength { + return fmt.Errorf("raw encryption key must decode to %d bytes", PBKDF2KeyLength) + } + return nil + } + if len(key) < 32 { return fmt.Errorf("encryption key must be at least 32 characters long") } diff --git a/pkg/encryption/encryption_test.go b/pkg/encryption/encryption_test.go index ca096ea..3ab69a4 100644 --- a/pkg/encryption/encryption_test.go +++ b/pkg/encryption/encryption_test.go @@ -1,6 +1,8 @@ package encryption import ( + "bytes" + "encoding/hex" "testing" "time" ) @@ -240,3 +242,95 @@ func TestConstantTimeCompare(t *testing.T) { t.Error("Empty slices should compare equal") } } + +func TestResolveKeyRaw(t *testing.T) { + raw, err := GenerateRawKey() + if err != nil { + t.Fatalf("GenerateRawKey: %v", err) + } + if !IsRawKey(raw) { + t.Fatalf("generated key is not raw: %q", raw[:4]) + } + key, err := ResolveKey(raw) + if err != nil { + t.Fatalf("ResolveKey raw: %v", err) + } + if len(key) != 32 { + t.Fatalf("raw key length %d", len(key)) + } + decoded, err := hex.DecodeString(raw[len(RawKeyPrefix):]) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(key, decoded) { + t.Fatal("raw key was not hex-decoded as-is") + } + if bytes.Equal(key, DeriveKey(raw)) { + t.Fatal("raw key must not go through PBKDF2") + } +} + +func TestResolveKeyLegacyHexStillPBKDF2(t *testing.T) { + legacy := "a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03" + decoded, err := hex.DecodeString(legacy) + if err != nil { + t.Fatal(err) + } + got, err := ResolveKey(legacy) + if err != nil { + t.Fatalf("ResolveKey legacy hex: %v", err) + } + want := DeriveKey(legacy) + if !bytes.Equal(got, want) { + t.Fatal("unprefixed 64-hex must still use PBKDF2 (old --generate-key configs)") + } + if bytes.Equal(got, decoded) { + t.Fatal("unprefixed 64-hex must not be treated as a raw AES key") + } +} + +func TestResolveKeyPassphrase(t *testing.T) { + pw := "test-passphrase-not-a-hex-key-value" + got, err := ResolveKey(pw) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, DeriveKey(pw)) { + t.Fatal("passphrase should use PBKDF2") + } +} + +func TestDeriveKeyCached(t *testing.T) { + pw := "cache-me-please-this-is-long-enough" + start := time.Now() + k1 := DeriveKey(pw) + first := time.Since(start) + start = time.Now() + k2 := DeriveKey(pw) + second := time.Since(start) + if !bytes.Equal(k1, k2) { + t.Fatal("cached key mismatch") + } + if first < 10*time.Millisecond { + t.Logf("first PBKDF2 unexpectedly fast: %v", first) + } + if second > 5*time.Millisecond { + t.Fatalf("cached DeriveKey too slow: first=%v second=%v", first, second) + } +} + +func TestValidateRawEncryptionKey(t *testing.T) { + raw, err := GenerateRawKey() + if err != nil { + t.Fatal(err) + } + if err := ValidateEncryptionKey(raw); err != nil { + t.Fatalf("valid raw key rejected: %v", err) + } + if err := ValidateEncryptionKey("raw:not-hex"); err == nil { + t.Fatal("invalid raw hex should fail") + } + if err := ValidateEncryptionKey("raw:abcd"); err == nil { + t.Fatal("short raw key should fail") + } +}