Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d8384c0f9 | |||
| bc486eb49d | |||
| 45778ac528 | |||
| c20398642d | |||
| 283781fd47 | |||
| 1fef0b6a70 | |||
| 6d259180fd | |||
| fa89d7c126 | |||
| e12142633a |
@@ -1,6 +1,9 @@
|
|||||||
name: PR Check
|
name: CI
|
||||||
on:
|
on:
|
||||||
- pull_request
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-and-test:
|
check-and-test:
|
||||||
@@ -10,6 +13,6 @@ jobs:
|
|||||||
- uses: actions/setup-go@main
|
- uses: actions/setup-go@main
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: 'go.mod'
|
||||||
- run: go mod tidy
|
- run: go mod tidy
|
||||||
- run: go build ./...
|
- run: go build ./...
|
||||||
- run: go test -race -v -shuffle=on ./...
|
- run: go test -race -v -shuffle=on ./...
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Flow: User → Client:8080,2222 → Encrypted Tunnel (Port 9000) → Server:80,2
|
|||||||
**Server Configuration** (`server.yaml`):
|
**Server Configuration** (`server.yaml`):
|
||||||
```yaml
|
```yaml
|
||||||
instance_id: teleport-server-01
|
instance_id: teleport-server-01
|
||||||
listen_address: :9000
|
listen_address: 127.0.0.1:9000 # loopback; use 0.0.0.0:9000 to publish
|
||||||
remote_address: ""
|
remote_address: ""
|
||||||
ports:
|
ports:
|
||||||
- "tcp://192.168.1.100:80"
|
- "tcp://192.168.1.100:80"
|
||||||
@@ -50,6 +50,7 @@ write_timeout: 30s
|
|||||||
instance_id: teleport-client-01
|
instance_id: teleport-client-01
|
||||||
listen_address: ""
|
listen_address: ""
|
||||||
remote_address: server.example.com:9000
|
remote_address: server.example.com:9000
|
||||||
|
bind_address: 127.0.0.1
|
||||||
ports:
|
ports:
|
||||||
- "tcp://80:8080"
|
- "tcp://80:8080"
|
||||||
- "tcp://22:2222"
|
- "tcp://22:2222"
|
||||||
@@ -59,15 +60,17 @@ read_timeout: 30s
|
|||||||
write_timeout: 30s
|
write_timeout: 30s
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
- User connects to `localhost:8080` → Traffic flows through encrypted tunnel on port 9000 → Server forwards to remote web server at `192.168.1.100:80`
|
- User connects to `127.0.0.1:8080` (client local listener, loopback by default) → Traffic flows through encrypted tunnel on `127.0.0.1:9000` → Server forwards to remote web server at `192.168.1.100:80`
|
||||||
- User connects to `localhost:2222` → Traffic flows through same encrypted tunnel on port 9000 → Server forwards to remote SSH server at `192.168.1.200:22`
|
- User connects to `127.0.0.1:2222` → Traffic flows through the same encrypted tunnel → Server forwards to remote SSH server at `192.168.1.200:22`
|
||||||
|
|
||||||
|
Local client ports bind `127.0.0.1` unless you set `bind_address: 0.0.0.0` or a per-rule host such as `"tcp://22:0.0.0.0:2222"`. Server `listen_address: 127.0.0.1:9000` is loopback-only; `0.0.0.0:9000` (or `:9000`) publishes the tunnel on all interfaces and must be explicit.
|
||||||
|
|
||||||
Both services share the same encrypted tunnel connection!
|
Both services share the same encrypted tunnel connection!
|
||||||
|
|
||||||
## Features
|
## 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)
|
- **Port Forwarding**: Forward multiple ports with different protocols (TCP and UDP)
|
||||||
- **Configuration-based**: Easy configuration via YAML files
|
- **Configuration-based**: Easy configuration via YAML files
|
||||||
- **Bidirectional**: Full bidirectional port forwarding
|
- **Bidirectional**: Full bidirectional port forwarding
|
||||||
@@ -116,7 +119,7 @@ Generate example configuration files:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
instance_id: teleport-server-01
|
instance_id: teleport-server-01
|
||||||
listen_address: :8080
|
listen_address: 127.0.0.1:8080 # use 0.0.0.0:8080 to accept remote clients
|
||||||
remote_address: ""
|
remote_address: ""
|
||||||
ports:
|
ports:
|
||||||
- "tcp://localhost:80"
|
- "tcp://localhost:80"
|
||||||
@@ -143,6 +146,7 @@ dns_server:
|
|||||||
instance_id: teleport-client-01
|
instance_id: teleport-client-01
|
||||||
listen_address: ""
|
listen_address: ""
|
||||||
remote_address: localhost:8080
|
remote_address: localhost:8080
|
||||||
|
bind_address: 127.0.0.1
|
||||||
ports:
|
ports:
|
||||||
- "tcp://80:8080"
|
- "tcp://80:8080"
|
||||||
encryption_key: your-secure-encryption-key-change-this-to-something-random
|
encryption_key: your-secure-encryption-key-change-this-to-something-random
|
||||||
@@ -169,18 +173,21 @@ dns_server:
|
|||||||
## Configuration Fields
|
## Configuration Fields
|
||||||
|
|
||||||
- `instance_id`: Unique identifier for this teleport instance
|
- `instance_id`: Unique identifier for this teleport instance
|
||||||
- `listen_address`: Address to listen on (server mode) - format: `host:port`
|
- `listen_address`: Address the **server** binds for the encrypted tunnel (`host:port`). Examples use `127.0.0.1:9000`. Use `0.0.0.0:9000` or `:9000` only when you intend to publish the server on all interfaces.
|
||||||
- `remote_address`: Address of remote teleport server (client mode) - format: `host:port`
|
- `remote_address`: Address of remote teleport server (client mode) - format: `host:port`
|
||||||
|
- `bind_address`: Host for **client** local TCP/UDP listeners (default: `127.0.0.1`). Set to `0.0.0.0` to accept connections from other machines. Publishing on all interfaces is never implicit.
|
||||||
- `ports`: Array of port forwarding rules in URL-style format
|
- `ports`: Array of port forwarding rules in URL-style format
|
||||||
- **Server format**: `protocol://target:targetport` - forwards to remote target
|
- **Server format**: `protocol://target:targetport` - forwards to remote target
|
||||||
- **Client format**: `protocol://targetport:localport` - forwards to teleport server's targetport, listens on localport
|
- **Client format**: `protocol://targetport:localport` - forwards to teleport server's targetport, listens on `bind_address:localport` (default `127.0.0.1`)
|
||||||
|
- **Client with bind**: `protocol://targetport:bindhost:localport` - per-rule listen host, e.g. `"tcp://22:0.0.0.0:2222"`
|
||||||
- Examples:
|
- Examples:
|
||||||
- `"tcp://localhost:80"` (server) - listen on port 80, forward to localhost:80
|
- `"tcp://localhost:80"` (server) - forward to localhost:80
|
||||||
- `"tcp://server-a:22"` (server) - listen on port 22, forward to server-a:22
|
- `"tcp://server-a:22"` (server) - forward to server-a:22
|
||||||
- `"tcp://80:8080"` (client) - listen on port 8080, forward to teleport server's port 80
|
- `"tcp://80:8080"` (client) - listen on `127.0.0.1:8080`, forward to teleport server's port 80
|
||||||
- `"udp://53:5353"` (client) - listen on port 5353, forward to teleport server's port 53
|
- `"tcp://22:0.0.0.0:2222"` (client) - listen on all interfaces port 2222
|
||||||
- `encryption_key`: Shared secret key for encryption (must be the same on both sides)
|
- `"udp://53:5353"` (client) - listen on `127.0.0.1:5353`, forward to teleport server's port 53
|
||||||
- `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
|
- `read_timeout`: Read timeout duration
|
||||||
- `write_timeout`: Write timeout duration
|
- `write_timeout`: Write timeout duration
|
||||||
- `max_connections`: Maximum concurrent connections (default: 1000 for server, 100 for client)
|
- `max_connections`: Maximum concurrent connections (default: 1000 for server, 100 for client)
|
||||||
@@ -192,6 +199,7 @@ dns_server:
|
|||||||
- `dns_server`: DNS server configuration
|
- `dns_server`: DNS server configuration
|
||||||
- `enabled`: Enable built-in DNS server
|
- `enabled`: Enable built-in DNS server
|
||||||
- `listen_port`: Port for DNS server to listen on
|
- `listen_port`: Port for DNS server to listen on
|
||||||
|
- `bind_address`: DNS listen host (default: `127.0.0.1`). Set to `0.0.0.0` to serve DNS on all interfaces; the default is not an open recursive forwarder on every interface.
|
||||||
- `backup_server`: Backup DNS server for fallback (e.g., "8.8.8.8:53")
|
- `backup_server`: Backup DNS server for fallback (e.g., "8.8.8.8:53")
|
||||||
- `custom_records`: Array of custom DNS records
|
- `custom_records`: Array of custom DNS records
|
||||||
- `name`: Domain name (e.g., "example.com")
|
- `name`: Domain name (e.g., "example.com")
|
||||||
@@ -257,7 +265,9 @@ dns_server:
|
|||||||
./teleport -k
|
./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
|
### Logging Options
|
||||||
|
|
||||||
@@ -278,14 +288,15 @@ Teleport uses URL-style port mapping format for cleaner configuration:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ports:
|
ports:
|
||||||
- "tcp://80:8080" # Listen on port 8080, forward to teleport server's port 80
|
- "tcp://80:8080" # Listen on 127.0.0.1:8080, forward to teleport server's port 80
|
||||||
- "udp://53:5353" # Listen on port 5353, forward to teleport server's port 53
|
- "udp://53:5353" # Listen on 127.0.0.1:5353, forward to teleport server's port 53
|
||||||
- "tcp://22:2222" # Listen on port 2222, forward to teleport server's port 22
|
- "tcp://22:0.0.0.0:2222" # Explicit all-interfaces bind
|
||||||
```
|
```
|
||||||
|
|
||||||
**Format**:
|
**Format**:
|
||||||
- **Server**: `"protocol://target:targetport"` - listen on targetport, forward to target:targetport
|
- **Server**: `"protocol://target:targetport"` - forward to target:targetport
|
||||||
- **Client**: `"protocol://targetport:localport"` - listen on localport, forward to teleport server's targetport
|
- **Client**: `"protocol://targetport:localport"` - listen on `127.0.0.1:localport` (or `bind_address`), forward to teleport server's targetport
|
||||||
|
- **Client with bind**: `"protocol://targetport:bindhost:localport"` - listen on bindhost:localport
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```yaml
|
```yaml
|
||||||
@@ -295,11 +306,12 @@ ports:
|
|||||||
- "tcp://server-a:22" # Listen on port 22, forward to server-a:22
|
- "tcp://server-a:22" # Listen on port 22, forward to server-a:22
|
||||||
- "udp://dns-server:53" # Listen on port 53, forward to dns-server:53
|
- "udp://dns-server:53" # Listen on port 53, forward to dns-server:53
|
||||||
|
|
||||||
# Client configurations
|
# Client configurations
|
||||||
ports:
|
ports:
|
||||||
- "tcp://80:8080" # Listen on port 8080, forward to teleport server's port 80
|
- "tcp://80:8080" # Listen on 127.0.0.1:8080, forward to teleport server's port 80
|
||||||
- "tcp://22:2222" # Listen on port 2222, forward to teleport server's port 22
|
- "tcp://22:2222" # Listen on 127.0.0.1:2222, forward to teleport server's port 22
|
||||||
- "udp://53:5353" # Listen on port 5353, forward to teleport server's port 53
|
- "tcp://22:0.0.0.0:2222" # Listen on all interfaces (explicit)
|
||||||
|
- "udp://53:5353" # Listen on 127.0.0.1:5353, forward to teleport server's port 53
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example: Remote SSH Access**
|
**Example: Remote SSH Access**
|
||||||
@@ -308,10 +320,10 @@ To access SSH on Server A through teleport server on Server B:
|
|||||||
**Server B configuration:**
|
**Server B configuration:**
|
||||||
```yaml
|
```yaml
|
||||||
instance_id: teleport-server-b
|
instance_id: teleport-server-b
|
||||||
listen_address: :8080
|
listen_address: 127.0.0.1:8080
|
||||||
remote_address: ""
|
remote_address: ""
|
||||||
ports:
|
ports:
|
||||||
- "tcp://server-a:22" # Listen on port 22, forward to server-a:22
|
- "tcp://server-a:22" # Forward to server-a:22
|
||||||
encryption_key: your-shared-key
|
encryption_key: your-shared-key
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -320,8 +332,9 @@ encryption_key: your-shared-key
|
|||||||
instance_id: teleport-client-c
|
instance_id: teleport-client-c
|
||||||
listen_address: ""
|
listen_address: ""
|
||||||
remote_address: server-b:8080
|
remote_address: server-b:8080
|
||||||
|
bind_address: 127.0.0.1
|
||||||
ports:
|
ports:
|
||||||
- "tcp://22:2222" # Listen on local port 2222, forward to teleport server's port 22
|
- "tcp://22:2222" # Listen on 127.0.0.1:2222, forward to teleport server's port 22
|
||||||
encryption_key: your-shared-key
|
encryption_key: your-shared-key
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -345,7 +358,7 @@ The program validates your configuration and will show clear error messages if:
|
|||||||
|
|
||||||
**Valid Server Configuration:**
|
**Valid Server Configuration:**
|
||||||
```yaml
|
```yaml
|
||||||
listen_address: :8080
|
listen_address: 127.0.0.1:8080
|
||||||
remote_address: ""
|
remote_address: ""
|
||||||
ports:
|
ports:
|
||||||
- "tcp://localhost:22"
|
- "tcp://localhost:22"
|
||||||
@@ -355,6 +368,7 @@ ports:
|
|||||||
```yaml
|
```yaml
|
||||||
listen_address: ""
|
listen_address: ""
|
||||||
remote_address: server:8080
|
remote_address: server:8080
|
||||||
|
bind_address: 127.0.0.1
|
||||||
ports:
|
ports:
|
||||||
- "tcp://22:2222"
|
- "tcp://22:2222"
|
||||||
```
|
```
|
||||||
@@ -369,6 +383,7 @@ ports:
|
|||||||
- Encryption keys are validated for entropy and strength
|
- Encryption keys are validated for entropy and strength
|
||||||
- Logging includes automatic sanitization of sensitive data
|
- Logging includes automatic sanitization of sensitive data
|
||||||
- Rate limiting prevents abuse and DoS attacks
|
- Rate limiting prevents abuse and DoS attacks
|
||||||
|
- Client local listeners and the built-in DNS server bind `127.0.0.1` by default so LAN/internet hosts cannot use your tunnel unless you set `bind_address` / a per-rule host to `0.0.0.0`
|
||||||
|
|
||||||
## Example Use Cases
|
## Example Use Cases
|
||||||
|
|
||||||
@@ -394,7 +409,7 @@ ports:
|
|||||||
- Generate: `./teleport -generate-config -config teleport-client.yaml`
|
- Generate: `./teleport -generate-config -config teleport-client.yaml`
|
||||||
- Edit the configuration file with local ports and remote server address
|
- Edit the configuration file with local ports and remote server address
|
||||||
- Run: `./teleport -config teleport-client.yaml`
|
- Run: `./teleport -config teleport-client.yaml`
|
||||||
- Connect to `localhost:local_port` to access the remote service
|
- Connect to `127.0.0.1:local_port` (loopback by default) to access the remote service
|
||||||
|
|
||||||
## Multi-Client Support
|
## Multi-Client Support
|
||||||
|
|
||||||
@@ -428,6 +443,7 @@ The built-in DNS server provides:
|
|||||||
dns_server:
|
dns_server:
|
||||||
enabled: true
|
enabled: true
|
||||||
listen_port: 5353
|
listen_port: 5353
|
||||||
|
bind_address: 127.0.0.1
|
||||||
backup_server: 8.8.8.8:53
|
backup_server: 8.8.8.8:53
|
||||||
custom_records:
|
custom_records:
|
||||||
- name: api.local
|
- name: api.local
|
||||||
@@ -489,6 +505,8 @@ rate_limit:
|
|||||||
window_size: 1s # Time window for rate limiting
|
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
|
### Advanced Logging
|
||||||
|
|
||||||
@@ -509,9 +527,9 @@ Teleport includes sophisticated logging with:
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The encryption key must be identical on both server and client
|
- 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 connections
|
- 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
|
- The client connects to the remote server and forwards local connections from `bind_address` (default `127.0.0.1`)
|
||||||
- All port forwarding is bidirectional
|
- All port forwarding is bidirectional
|
||||||
- Port format uses URL-style conventions: `"protocol://target:port"`
|
- Port format uses URL-style conventions: `"protocol://target:port"`
|
||||||
- Configuration files use YAML format for better readability
|
- Configuration files use YAML format for better readability
|
||||||
|
|||||||
+6
-14
@@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -62,7 +60,8 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("Generated encryption key: %s\n", key)
|
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
|
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) {
|
func generateRandomKey() (string, error) {
|
||||||
// Generate 32 random bytes (256 bits) for a strong encryption key
|
key, err := encryption.GenerateRawKey()
|
||||||
bytes := make([]byte, 32)
|
if err != nil {
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
return "", err
|
||||||
return "", fmt.Errorf("failed to generate random key: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to hexadecimal string for easy copying
|
|
||||||
key := hex.EncodeToString(bytes)
|
|
||||||
|
|
||||||
// Validate the generated key
|
|
||||||
if err := encryption.ValidateEncryptionKey(key); err != nil {
|
if err := encryption.ValidateEncryptionKey(key); err != nil {
|
||||||
return "", fmt.Errorf("generated key failed validation: %v", err)
|
return "", fmt.Errorf("generated key failed validation: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module teleport
|
module teleport
|
||||||
|
|
||||||
go 1.23.5
|
go 1.27.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/miekg/dns v1.1.68
|
github.com/miekg/dns v1.1.68
|
||||||
|
|||||||
+74
-21
@@ -28,6 +28,7 @@ type TeleportClient struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
connectionPool chan net.Conn
|
connectionPool chan net.Conn
|
||||||
maxPoolSize int
|
maxPoolSize int
|
||||||
|
derivedKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTeleportClient creates a new teleport client
|
// NewTeleportClient creates a new teleport client
|
||||||
@@ -49,6 +50,7 @@ func NewTeleportClient(config *config.Config) *TeleportClient {
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
connectionPool: make(chan net.Conn, maxPoolSize),
|
connectionPool: make(chan net.Conn, maxPoolSize),
|
||||||
maxPoolSize: maxPoolSize,
|
maxPoolSize: maxPoolSize,
|
||||||
|
derivedKey: mustResolveKey(config.EncryptionKey),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ func (tc *TeleportClient) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to server: %v", err)
|
return fmt.Errorf("failed to connect to server: %v", err)
|
||||||
}
|
}
|
||||||
|
config.ApplyTCPKeepAlive(conn, tc.config.KeepAlive)
|
||||||
tc.serverConn = conn
|
tc.serverConn = conn
|
||||||
|
|
||||||
// Start DNS server if enabled
|
// Start DNS server if enabled
|
||||||
@@ -91,9 +94,11 @@ func (tc *TeleportClient) startPortForwarding(rule config.PortRule) {
|
|||||||
|
|
||||||
// startTCPForwarding starts TCP port forwarding
|
// startTCPForwarding starts TCP port forwarding
|
||||||
func (tc *TeleportClient) startTCPForwarding(rule config.PortRule) {
|
func (tc *TeleportClient) startTCPForwarding(rule config.PortRule) {
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", rule.LocalPort))
|
listenAddr := tc.config.ClientListenAddr(rule)
|
||||||
|
listener, err := net.Listen("tcp", listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
|
"addr": listenAddr,
|
||||||
"port": rule.LocalPort,
|
"port": rule.LocalPort,
|
||||||
"error": err,
|
"error": err,
|
||||||
}).Error("Failed to start TCP listener")
|
}).Error("Failed to start TCP listener")
|
||||||
@@ -102,6 +107,7 @@ func (tc *TeleportClient) startTCPForwarding(rule config.PortRule) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
|
"listen_addr": listenAddr,
|
||||||
"local_port": rule.LocalPort,
|
"local_port": rule.LocalPort,
|
||||||
"remote_addr": tc.config.RemoteAddress,
|
"remote_addr": tc.config.RemoteAddress,
|
||||||
"remote_port": rule.RemotePort,
|
"remote_port": rule.RemotePort,
|
||||||
@@ -182,6 +188,7 @@ func (tc *TeleportClient) getConnection() (net.Conn, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create connection: %v", err)
|
return nil, fmt.Errorf("failed to create connection: %v", err)
|
||||||
}
|
}
|
||||||
|
config.ApplyTCPKeepAlive(conn, tc.config.KeepAlive)
|
||||||
|
|
||||||
// Set connection timeouts
|
// Set connection timeouts
|
||||||
if tc.config.ReadTimeout > 0 {
|
if tc.config.ReadTimeout > 0 {
|
||||||
@@ -252,9 +259,11 @@ func (tc *TeleportClient) returnConnection(conn net.Conn) {
|
|||||||
|
|
||||||
// startUDPForwarding starts UDP port forwarding
|
// startUDPForwarding starts UDP port forwarding
|
||||||
func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) {
|
func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) {
|
||||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", rule.LocalPort))
|
listenAddr := tc.config.ClientListenAddr(rule)
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
|
"addr": listenAddr,
|
||||||
"port": rule.LocalPort,
|
"port": rule.LocalPort,
|
||||||
"error": err,
|
"error": err,
|
||||||
}).Error("Failed to resolve UDP address")
|
}).Error("Failed to resolve UDP address")
|
||||||
@@ -264,6 +273,7 @@ func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) {
|
|||||||
conn, err := net.ListenUDP("udp", addr)
|
conn, err := net.ListenUDP("udp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
|
"addr": listenAddr,
|
||||||
"port": rule.LocalPort,
|
"port": rule.LocalPort,
|
||||||
"error": err,
|
"error": err,
|
||||||
}).Error("Failed to start UDP listener")
|
}).Error("Failed to start UDP listener")
|
||||||
@@ -275,6 +285,7 @@ func (tc *TeleportClient) startUDPForwarding(rule config.PortRule) {
|
|||||||
tc.udpMutex.Unlock()
|
tc.udpMutex.Unlock()
|
||||||
|
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
|
"listen_addr": listenAddr,
|
||||||
"local_port": rule.LocalPort,
|
"local_port": rule.LocalPort,
|
||||||
"remote_addr": tc.config.RemoteAddress,
|
"remote_addr": tc.config.RemoteAddress,
|
||||||
"remote_port": rule.RemotePort,
|
"remote_port": rule.RemotePort,
|
||||||
@@ -334,6 +345,7 @@ func (tc *TeleportClient) sendTaggedUDPPacketWithResponse(packet types.TaggedUDP
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer serverConn.Close()
|
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")
|
logger.WithField("packetID", packet.Header.PacketID).Debug("UDP CLIENT: Connected to server, sending port forward request")
|
||||||
|
|
||||||
@@ -390,9 +402,8 @@ func (tc *TeleportClient) sendTaggedUDPPacketToConnection(conn net.Conn, packet
|
|||||||
"data_length": len(data),
|
"data_length": len(data),
|
||||||
}).Debug("UDP CLIENT: Serialized packet")
|
}).Debug("UDP CLIENT: Serialized packet")
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data (key resolved once at process start)
|
||||||
key := encryption.DeriveKey(tc.config.EncryptionKey)
|
encryptedData, err := encryption.EncryptData(data, tc.derivedKey)
|
||||||
encryptedData, err := encryption.EncryptData(data, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
"packetID": packet.Header.PacketID,
|
"packetID": packet.Header.PacketID,
|
||||||
@@ -444,9 +455,8 @@ func (tc *TeleportClient) waitForUDPResponseAndForward(conn net.Conn, expectedPa
|
|||||||
"bytes_received": n,
|
"bytes_received": n,
|
||||||
}).Debug("UDP CLIENT: Received response bytes")
|
}).Debug("UDP CLIENT: Received response bytes")
|
||||||
|
|
||||||
// Decrypt the response
|
// Decrypt the response (key resolved once at process start)
|
||||||
key := encryption.DeriveKey(tc.config.EncryptionKey)
|
decryptedData, err := encryption.DecryptData(buffer[:n], tc.derivedKey)
|
||||||
decryptedData, err := encryption.DecryptData(buffer[:n], key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
"packetID": expectedPacketID,
|
"packetID": expectedPacketID,
|
||||||
@@ -629,6 +639,7 @@ func (tc *TeleportClient) deserializeTaggedUDPPacket(data []byte) (types.TaggedU
|
|||||||
// handleTCPConnection handles a TCP connection from a local client
|
// handleTCPConnection handles a TCP connection from a local client
|
||||||
func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.PortRule) {
|
func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.PortRule) {
|
||||||
defer clientConn.Close()
|
defer clientConn.Close()
|
||||||
|
config.ApplyTCPKeepAlive(clientConn, tc.config.KeepAlive)
|
||||||
|
|
||||||
// Get a connection from the pool or create a new one
|
// Get a connection from the pool or create a new one
|
||||||
serverConn, err := tc.getConnection()
|
serverConn, err := tc.getConnection()
|
||||||
@@ -639,7 +650,12 @@ func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.P
|
|||||||
defer tc.returnConnection(serverConn)
|
defer tc.returnConnection(serverConn)
|
||||||
|
|
||||||
// Send port forward request to server
|
// Send port forward request to server
|
||||||
request := types.PortForwardRequest(rule)
|
request := types.PortForwardRequest{
|
||||||
|
LocalPort: rule.LocalPort,
|
||||||
|
RemotePort: rule.RemotePort,
|
||||||
|
Protocol: rule.Protocol,
|
||||||
|
TargetHost: rule.TargetHost,
|
||||||
|
}
|
||||||
|
|
||||||
if err := tc.sendRequestToConnection(serverConn, request); err != nil {
|
if err := tc.sendRequestToConnection(serverConn, request); err != nil {
|
||||||
logger.WithField("error", err).Error("Failed to send port forward request")
|
logger.WithField("error", err).Error("Failed to send port forward request")
|
||||||
@@ -650,23 +666,34 @@ func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.P
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
|
|
||||||
// Forward data from client to server
|
// Encrypt local plaintext onto the tunnel
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
tc.forwardData(clientConn, serverConn)
|
tc.forwardData(clientConn, serverConn, true)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Forward data from server to client
|
// Decrypt tunnel frames to the local connection
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
tc.forwardData(serverConn, clientConn)
|
tc.forwardData(serverConn, clientConn, false)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// forwardData forwards data from src to dst
|
// forwardData copies between a local/plaintext conn and the tunnel.
|
||||||
func (tc *TeleportClient) forwardData(src, dst net.Conn) {
|
// When toTunnel is true, plaintext is AES-GCM encrypted and written as
|
||||||
|
// uint32 length + ciphertext. When false, framed ciphertext is decrypted
|
||||||
|
// and written as plaintext.
|
||||||
|
func (tc *TeleportClient) forwardData(src, dst net.Conn, toTunnel bool) {
|
||||||
|
if toTunnel {
|
||||||
|
tc.forwardPlainToTunnel(src, dst)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tc.forwardTunnelToPlain(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TeleportClient) forwardPlainToTunnel(src, dst net.Conn) {
|
||||||
buffer := make([]byte, 4096)
|
buffer := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -675,14 +702,32 @@ func (tc *TeleportClient) forwardData(src, dst net.Conn) {
|
|||||||
default:
|
default:
|
||||||
n, err := src.Read(buffer)
|
n, err := src.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Close the destination connection when source closes
|
|
||||||
dst.Close()
|
dst.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if n == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := encryption.WriteEncryptedFrame(dst, buffer[:n], tc.derivedKey); err != nil {
|
||||||
|
src.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err = dst.Write(buffer[:n])
|
func (tc *TeleportClient) forwardTunnelToPlain(src, dst net.Conn) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tc.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
plain, err := encryption.ReadEncryptedFrame(src, tc.derivedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Close the source connection when destination closes
|
dst.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := dst.Write(plain); err != nil {
|
||||||
src.Close()
|
src.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -703,9 +748,8 @@ func (tc *TeleportClient) sendRequestToConnection(conn net.Conn, request types.P
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data (key derived once at process start)
|
||||||
key := encryption.DeriveKey(tc.config.EncryptionKey)
|
encryptedData, err := encryption.EncryptData(data, tc.derivedKey)
|
||||||
encryptedData, err := encryption.EncryptData(data, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
"error": err,
|
"error": err,
|
||||||
@@ -783,3 +827,12 @@ func (tc *TeleportClient) serializeRequest(request types.PortForwardRequest) ([]
|
|||||||
|
|
||||||
return data, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"teleport/pkg/config"
|
||||||
|
"teleport/pkg/encryption"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testClient(t *testing.T) *TeleportClient {
|
||||||
|
t.Helper()
|
||||||
|
return NewTeleportClient(&config.Config{
|
||||||
|
EncryptionKey: "a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardDataHidesHTTPPlaintextOnTunnel(t *testing.T) {
|
||||||
|
tc := testClient(t)
|
||||||
|
defer tc.Stop()
|
||||||
|
|
||||||
|
localA, localB := net.Pipe()
|
||||||
|
tunnelA, tunnelB := net.Pipe()
|
||||||
|
defer localA.Close()
|
||||||
|
defer localB.Close()
|
||||||
|
defer tunnelA.Close()
|
||||||
|
defer tunnelB.Close()
|
||||||
|
|
||||||
|
go tc.forwardData(localA, tunnelA, true)
|
||||||
|
|
||||||
|
req := []byte("GET /secret HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
||||||
|
writeErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := localB.Write(req)
|
||||||
|
writeErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
tunnelB.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(tunnelB, binary.BigEndian, &length); err != nil {
|
||||||
|
t.Fatalf("failed to read frame length from tunnel: %v", err)
|
||||||
|
}
|
||||||
|
if length == 0 || length > encryption.MaxFrameSize {
|
||||||
|
t.Fatalf("unexpected frame length %d", length)
|
||||||
|
}
|
||||||
|
ciphertext := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(tunnelB, ciphertext); err != nil {
|
||||||
|
t.Fatalf("failed to read ciphertext from tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Contains(ciphertext, []byte("GET /")) ||
|
||||||
|
bytes.Contains(ciphertext, []byte("Host:")) ||
|
||||||
|
bytes.Contains(ciphertext, []byte("example.com")) {
|
||||||
|
t.Fatal("HTTP plaintext visible on client->server tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame bytes.Buffer
|
||||||
|
if err := binary.Write(&frame, binary.BigEndian, length); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
frame.Write(ciphertext)
|
||||||
|
plain, err := encryption.ReadEncryptedFrame(&frame, tc.derivedKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt of tunneled frame failed: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plain, req) {
|
||||||
|
t.Errorf("decrypted payload mismatch: got %q want %q", plain, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-writeErr:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write to local conn failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for local write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardDataDecryptsTunnelToLocal(t *testing.T) {
|
||||||
|
tc := testClient(t)
|
||||||
|
defer tc.Stop()
|
||||||
|
|
||||||
|
tunnelA, tunnelB := net.Pipe()
|
||||||
|
localA, localB := net.Pipe()
|
||||||
|
defer tunnelA.Close()
|
||||||
|
defer tunnelB.Close()
|
||||||
|
defer localA.Close()
|
||||||
|
defer localB.Close()
|
||||||
|
|
||||||
|
go tc.forwardData(tunnelA, localA, false)
|
||||||
|
|
||||||
|
payload := []byte("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello")
|
||||||
|
writeErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
writeErr <- encryption.WriteEncryptedFrame(tunnelB, payload, tc.derivedKey)
|
||||||
|
}()
|
||||||
|
|
||||||
|
localB.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
got := make([]byte, len(payload))
|
||||||
|
if _, err := io.ReadFull(localB, got); err != nil {
|
||||||
|
t.Fatalf("failed to read decrypted payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, payload) {
|
||||||
|
t.Errorf("got %q want %q", got, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-writeErr:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteEncryptedFrame failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for tunnel write")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"teleport/pkg/config"
|
||||||
|
"teleport/pkg/encryption"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testServer(t *testing.T) *TeleportServer {
|
||||||
|
t.Helper()
|
||||||
|
return NewTeleportServer(&config.Config{
|
||||||
|
EncryptionKey: "a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardDataHidesHTTPPlaintextOnTunnel(t *testing.T) {
|
||||||
|
ts := testServer(t)
|
||||||
|
defer ts.Stop()
|
||||||
|
|
||||||
|
targetA, targetB := net.Pipe()
|
||||||
|
tunnelA, tunnelB := net.Pipe()
|
||||||
|
defer targetA.Close()
|
||||||
|
defer targetB.Close()
|
||||||
|
defer tunnelA.Close()
|
||||||
|
defer tunnelB.Close()
|
||||||
|
|
||||||
|
// target -> tunnel (encrypt)
|
||||||
|
go ts.forwardData(targetA, tunnelA, true)
|
||||||
|
|
||||||
|
resp := []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello")
|
||||||
|
writeErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := targetB.Write(resp)
|
||||||
|
writeErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
tunnelB.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(tunnelB, binary.BigEndian, &length); err != nil {
|
||||||
|
t.Fatalf("failed to read frame length from tunnel: %v", err)
|
||||||
|
}
|
||||||
|
ciphertext := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(tunnelB, ciphertext); err != nil {
|
||||||
|
t.Fatalf("failed to read ciphertext from tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Contains(ciphertext, []byte("HTTP/1.1")) ||
|
||||||
|
bytes.Contains(ciphertext, []byte("Content-Type")) ||
|
||||||
|
bytes.Contains(ciphertext, []byte("hello")) {
|
||||||
|
t.Fatal("HTTP plaintext visible on server->client tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame bytes.Buffer
|
||||||
|
if err := binary.Write(&frame, binary.BigEndian, length); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
frame.Write(ciphertext)
|
||||||
|
plain, err := encryption.ReadEncryptedFrame(&frame, ts.derivedKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt of tunneled frame failed: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plain, resp) {
|
||||||
|
t.Errorf("decrypted payload mismatch: got %q want %q", plain, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-writeErr:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write to target conn failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for target write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardDataDecryptsTunnelToTarget(t *testing.T) {
|
||||||
|
ts := testServer(t)
|
||||||
|
defer ts.Stop()
|
||||||
|
|
||||||
|
tunnelA, tunnelB := net.Pipe()
|
||||||
|
targetA, targetB := net.Pipe()
|
||||||
|
defer tunnelA.Close()
|
||||||
|
defer tunnelB.Close()
|
||||||
|
defer targetA.Close()
|
||||||
|
defer targetB.Close()
|
||||||
|
|
||||||
|
go ts.forwardData(tunnelA, targetA, false)
|
||||||
|
|
||||||
|
req := []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
||||||
|
writeErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
writeErr <- encryption.WriteEncryptedFrame(tunnelB, req, ts.derivedKey)
|
||||||
|
}()
|
||||||
|
|
||||||
|
targetB.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
got := make([]byte, len(req))
|
||||||
|
if _, err := io.ReadFull(targetB, got); err != nil {
|
||||||
|
t.Fatalf("failed to read decrypted payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, req) {
|
||||||
|
t.Errorf("got %q want %q", got, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-writeErr:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteEncryptedFrame failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for tunnel write")
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-18
@@ -35,6 +35,7 @@ type TeleportServer struct {
|
|||||||
goroutineSem chan struct{} // Semaphore for limiting concurrent goroutines
|
goroutineSem chan struct{} // Semaphore for limiting concurrent goroutines
|
||||||
maxGoroutines int // Maximum concurrent goroutines
|
maxGoroutines int // Maximum concurrent goroutines
|
||||||
metrics *metrics.Metrics
|
metrics *metrics.Metrics
|
||||||
|
derivedKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTeleportServer creates a new teleport server
|
// NewTeleportServer creates a new teleport server
|
||||||
@@ -76,6 +77,7 @@ func NewTeleportServer(config *config.Config) *TeleportServer {
|
|||||||
goroutineSem: make(chan struct{}, maxGoroutines),
|
goroutineSem: make(chan struct{}, maxGoroutines),
|
||||||
maxGoroutines: maxGoroutines,
|
maxGoroutines: maxGoroutines,
|
||||||
metrics: metricsInstance,
|
metrics: metricsInstance,
|
||||||
|
derivedKey: mustResolveKey(config.EncryptionKey),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,9 +254,8 @@ func (ts *TeleportServer) sendTaggedUDPPacket(clientConn *net.UDPConn, packet ty
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data (key resolved once at process start)
|
||||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
encryptedData, err := encryption.EncryptData(data, ts.derivedKey)
|
||||||
encryptedData, err := encryption.EncryptData(data, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithField("error", err).Debug("Failed to encrypt UDP packet")
|
logger.WithField("error", err).Debug("Failed to encrypt UDP packet")
|
||||||
return
|
return
|
||||||
@@ -308,6 +309,8 @@ func (ts *TeleportServer) handleConnectionWithLimit(conn net.Conn) {
|
|||||||
ts.metrics.DecrementActiveConnections()
|
ts.metrics.DecrementActiveConnections()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
config.ApplyTCPKeepAlive(conn, ts.config.KeepAlive)
|
||||||
|
|
||||||
// Set connection timeouts
|
// Set connection timeouts
|
||||||
if ts.config.ReadTimeout > 0 {
|
if ts.config.ReadTimeout > 0 {
|
||||||
conn.SetReadDeadline(time.Now().Add(ts.config.ReadTimeout))
|
conn.SetReadDeadline(time.Now().Add(ts.config.ReadTimeout))
|
||||||
@@ -391,6 +394,7 @@ func (ts *TeleportServer) handleTCPForward(clientConn net.Conn, rule *config.Por
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer targetConn.Close()
|
defer targetConn.Close()
|
||||||
|
config.ApplyTCPKeepAlive(targetConn, ts.config.KeepAlive)
|
||||||
|
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
"client": clientConn.RemoteAddr(),
|
"client": clientConn.RemoteAddr(),
|
||||||
@@ -403,12 +407,12 @@ func (ts *TeleportServer) handleTCPForward(clientConn net.Conn, rule *config.Por
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ts.forwardData(clientConn, targetConn)
|
ts.forwardData(clientConn, targetConn, false) // decrypt from tunnel
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ts.forwardData(targetConn, clientConn)
|
ts.forwardData(targetConn, clientConn, true) // encrypt onto tunnel
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -476,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")
|
logger.WithField("bytes_received", n).Debug("UDP SERVER: Received bytes from client")
|
||||||
|
|
||||||
// Decrypt the data
|
// Decrypt the data (key resolved once at process start)
|
||||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
decryptedData, err := encryption.DecryptData(buffer[:n], ts.derivedKey)
|
||||||
decryptedData, err := encryption.DecryptData(buffer[:n], key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithField("error", err).Debug("UDP SERVER: Failed to decrypt UDP packet")
|
logger.WithField("error", err).Debug("UDP SERVER: Failed to decrypt UDP packet")
|
||||||
continue
|
continue
|
||||||
@@ -593,8 +596,7 @@ func (ts *TeleportServer) handleUDPForward(clientConn net.Conn, rule *config.Por
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
encryptedData, err := encryption.EncryptData(data, ts.derivedKey)
|
||||||
encryptedData, err := encryption.EncryptData(data, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
"packetID": originalPacketID,
|
"packetID": originalPacketID,
|
||||||
@@ -704,8 +706,19 @@ func (ts *TeleportServer) deserializeTaggedUDPPacket(data []byte) (types.TaggedU
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// forwardData forwards data between two connections
|
// forwardData copies between a local/target conn and the tunnel.
|
||||||
func (ts *TeleportServer) forwardData(src, dst net.Conn) {
|
// When toTunnel is true, plaintext is AES-GCM encrypted and written as
|
||||||
|
// uint32 length + ciphertext. When false, framed ciphertext is decrypted
|
||||||
|
// and written as plaintext.
|
||||||
|
func (ts *TeleportServer) forwardData(src, dst net.Conn, toTunnel bool) {
|
||||||
|
if toTunnel {
|
||||||
|
ts.forwardPlainToTunnel(src, dst)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.forwardTunnelToPlain(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TeleportServer) forwardPlainToTunnel(src, dst net.Conn) {
|
||||||
buffer := make([]byte, 4096)
|
buffer := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -714,14 +727,32 @@ func (ts *TeleportServer) forwardData(src, dst net.Conn) {
|
|||||||
default:
|
default:
|
||||||
n, err := src.Read(buffer)
|
n, err := src.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Close the destination connection when source closes
|
|
||||||
dst.Close()
|
dst.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if n == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := encryption.WriteEncryptedFrame(dst, buffer[:n], ts.derivedKey); err != nil {
|
||||||
|
src.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err = dst.Write(buffer[:n])
|
func (ts *TeleportServer) forwardTunnelToPlain(src, dst net.Conn) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ts.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
plain, err := encryption.ReadEncryptedFrame(src, ts.derivedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Close the source connection when destination closes
|
dst.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := dst.Write(plain); err != nil {
|
||||||
src.Close()
|
src.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -757,9 +788,8 @@ func (ts *TeleportServer) readRequest(conn net.Conn, request *types.PortForwardR
|
|||||||
bytesRead += n
|
bytesRead += n
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the data
|
// Decrypt the data (key derived once at process start)
|
||||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
decryptedData, err := encryption.DecryptData(encryptedData, ts.derivedKey)
|
||||||
decryptedData, err := encryption.DecryptData(encryptedData, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithFields(map[string]interface{}{
|
logger.WithFields(map[string]interface{}{
|
||||||
"error": err,
|
"error": err,
|
||||||
@@ -867,3 +897,12 @@ func (ts *TeleportServer) deserializeRequest(data []byte, request *types.PortFor
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
+125
-23
@@ -1,9 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,6 +18,7 @@ type Config struct {
|
|||||||
InstanceID string `yaml:"instance_id"`
|
InstanceID string `yaml:"instance_id"`
|
||||||
ListenAddress string `yaml:"listen_address"`
|
ListenAddress string `yaml:"listen_address"`
|
||||||
RemoteAddress string `yaml:"remote_address"`
|
RemoteAddress string `yaml:"remote_address"`
|
||||||
|
BindAddress string `yaml:"bind_address"` // local TCP/UDP/DNS bind host; default 127.0.0.1
|
||||||
Ports []PortRule `yaml:"ports"`
|
Ports []PortRule `yaml:"ports"`
|
||||||
EncryptionKey string `yaml:"encryption_key"`
|
EncryptionKey string `yaml:"encryption_key"`
|
||||||
KeepAlive bool `yaml:"keep_alive"`
|
KeepAlive bool `yaml:"keep_alive"`
|
||||||
@@ -31,10 +31,43 @@ type Config struct {
|
|||||||
|
|
||||||
// PortRule defines a port forwarding rule
|
// PortRule defines a port forwarding rule
|
||||||
type PortRule struct {
|
type PortRule struct {
|
||||||
LocalPort int `yaml:"local_port"`
|
LocalPort int `yaml:"local_port"`
|
||||||
RemotePort int `yaml:"remote_port"`
|
RemotePort int `yaml:"remote_port"`
|
||||||
Protocol string `yaml:"protocol"` // "tcp" or "udp"
|
Protocol string `yaml:"protocol"` // "tcp" or "udp"
|
||||||
TargetHost string `yaml:"target_host,omitempty"` // Target host for server-side forwarding (defaults to localhost)
|
TargetHost string `yaml:"target_host,omitempty"` // Target host for server-side forwarding (defaults to localhost)
|
||||||
|
BindAddress string `yaml:"-"` // Client local listen host; empty means use Config.BindAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBindAddress is the loopback host used when bind_address is omitted.
|
||||||
|
// Binding 0.0.0.0 (all interfaces) requires an explicit override.
|
||||||
|
const DefaultBindAddress = "127.0.0.1"
|
||||||
|
|
||||||
|
// NormalizeBindAddress returns host, or 127.0.0.1 when host is empty.
|
||||||
|
func NormalizeBindAddress(host string) string {
|
||||||
|
if host == "" {
|
||||||
|
return DefaultBindAddress
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalListenAddr returns host:port for a local listener. Empty host becomes 127.0.0.1.
|
||||||
|
func LocalListenAddr(host string, port int) string {
|
||||||
|
return net.JoinHostPort(NormalizeBindAddress(host), strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientListenAddr is the address the client binds for a port rule.
|
||||||
|
// Per-rule BindAddress wins over Config.BindAddress; both default to 127.0.0.1.
|
||||||
|
func (c *Config) ClientListenAddr(rule PortRule) string {
|
||||||
|
host := rule.BindAddress
|
||||||
|
if host == "" && c != nil {
|
||||||
|
host = c.BindAddress
|
||||||
|
}
|
||||||
|
return LocalListenAddr(host, rule.LocalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAddr is the address the built-in DNS server binds. Default 127.0.0.1.
|
||||||
|
func (d DNSServerConfig) ListenAddr() string {
|
||||||
|
return LocalListenAddr(d.BindAddress, d.ListenPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML implements custom YAML unmarshaling for PortRule
|
// UnmarshalYAML implements custom YAML unmarshaling for PortRule
|
||||||
@@ -133,21 +166,70 @@ func (p *PortRule) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
p.TargetHost = "" // Client doesn't specify target host
|
p.TargetHost = "" // Client doesn't specify target host
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
} else if len(addressParts) >= 3 {
|
||||||
|
// Client format with bind host: protocol://targetport:bindhost:localport
|
||||||
|
// e.g. tcp://22:127.0.0.1:2222 or tcp://22:0.0.0.0:2222
|
||||||
|
firstColon := strings.Index(addressPart, ":")
|
||||||
|
lastColon := strings.LastIndex(addressPart, ":")
|
||||||
|
if firstColon < 0 || lastColon <= firstColon {
|
||||||
|
return fmt.Errorf("invalid address format: %s (expected 'targetport:bindhost:localport')", addressPart)
|
||||||
|
}
|
||||||
|
targetPortStr := addressPart[:firstColon]
|
||||||
|
bindHost := addressPart[firstColon+1 : lastColon]
|
||||||
|
localPortStr := addressPart[lastColon+1:]
|
||||||
|
|
||||||
|
bindHost = strings.TrimPrefix(bindHost, "[")
|
||||||
|
bindHost = strings.TrimSuffix(bindHost, "]")
|
||||||
|
if bindHost == "" {
|
||||||
|
return fmt.Errorf("bind host is required in format 'targetport:bindhost:localport'")
|
||||||
|
}
|
||||||
|
if len(bindHost) > 253 {
|
||||||
|
return fmt.Errorf("bind host too long")
|
||||||
|
}
|
||||||
|
for _, c := range bindHost {
|
||||||
|
if c < 32 || c > 126 {
|
||||||
|
return fmt.Errorf("invalid character in bind host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPort, err := strconv.Atoi(targetPortStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid target port: %s", targetPortStr)
|
||||||
|
}
|
||||||
|
localPort, err := strconv.Atoi(localPortStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid local port: %s", localPortStr)
|
||||||
|
}
|
||||||
|
if targetPort < 1 || targetPort > 65535 {
|
||||||
|
return fmt.Errorf("invalid target port: %d (must be 1-65535)", targetPort)
|
||||||
|
}
|
||||||
|
if localPort < 1 || localPort > 65535 {
|
||||||
|
return fmt.Errorf("invalid local port: %d (must be 1-65535)", localPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LocalPort = localPort
|
||||||
|
p.RemotePort = targetPort
|
||||||
|
p.Protocol = protocol
|
||||||
|
p.TargetHost = ""
|
||||||
|
p.BindAddress = bindHost
|
||||||
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid address format: %s (expected 'target:port' for server or 'targetport:localport' for client)", addressPart)
|
return fmt.Errorf("invalid address format: %s (expected 'target:port' for server, 'targetport:localport' for client, or 'targetport:bindhost:localport' for client with bind)", addressPart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML implements custom YAML marshaling for PortRule
|
// MarshalYAML implements custom YAML marshaling for PortRule
|
||||||
func (p PortRule) MarshalYAML() (interface{}, error) {
|
func (p PortRule) MarshalYAML() (interface{}, error) {
|
||||||
// Use new format: protocol://target:targetport (server) or protocol://targetport:localport (client)
|
// Server: protocol://target:targetport
|
||||||
|
// Client: protocol://targetport:localport
|
||||||
|
// Client with explicit bind: protocol://targetport:bindhost:localport
|
||||||
if p.TargetHost == "" {
|
if p.TargetHost == "" {
|
||||||
// Client format: protocol://targetport:localport
|
if p.BindAddress != "" {
|
||||||
|
return fmt.Sprintf("%s://%d:%s:%d", p.Protocol, p.RemotePort, p.BindAddress, p.LocalPort), nil
|
||||||
|
}
|
||||||
return fmt.Sprintf("%s://%d:%d", p.Protocol, p.RemotePort, p.LocalPort), nil
|
return fmt.Sprintf("%s://%d:%d", p.Protocol, p.RemotePort, p.LocalPort), nil
|
||||||
} else {
|
|
||||||
// Server format: protocol://target:targetport
|
|
||||||
return fmt.Sprintf("%s://%s:%d", p.Protocol, p.TargetHost, p.RemotePort), nil
|
|
||||||
}
|
}
|
||||||
|
return fmt.Sprintf("%s://%s:%d", p.Protocol, p.TargetHost, p.RemotePort), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RateLimitConfig defines rate limiting configuration
|
// RateLimitConfig defines rate limiting configuration
|
||||||
@@ -162,6 +244,7 @@ type RateLimitConfig struct {
|
|||||||
type DNSServerConfig struct {
|
type DNSServerConfig struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
ListenPort int `yaml:"listen_port"`
|
ListenPort int `yaml:"listen_port"`
|
||||||
|
BindAddress string `yaml:"bind_address"`
|
||||||
BackupServer string `yaml:"backup_server"`
|
BackupServer string `yaml:"backup_server"`
|
||||||
CustomRecords []DNSRecord `yaml:"custom_records"`
|
CustomRecords []DNSRecord `yaml:"custom_records"`
|
||||||
}
|
}
|
||||||
@@ -217,9 +300,15 @@ func LoadConfig(filename string) (*Config, error) {
|
|||||||
if config.RateLimit.WindowSize == 0 {
|
if config.RateLimit.WindowSize == 0 {
|
||||||
config.RateLimit.WindowSize = 1 * time.Second
|
config.RateLimit.WindowSize = 1 * time.Second
|
||||||
}
|
}
|
||||||
|
if config.BindAddress == "" {
|
||||||
|
config.BindAddress = DefaultBindAddress
|
||||||
|
}
|
||||||
if config.DNSServer.ListenPort == 0 {
|
if config.DNSServer.ListenPort == 0 {
|
||||||
config.DNSServer.ListenPort = 5353
|
config.DNSServer.ListenPort = 5353
|
||||||
}
|
}
|
||||||
|
if config.DNSServer.BindAddress == "" {
|
||||||
|
config.DNSServer.BindAddress = DefaultBindAddress
|
||||||
|
}
|
||||||
if config.DNSServer.BackupServer == "" {
|
if config.DNSServer.BackupServer == "" {
|
||||||
config.DNSServer.BackupServer = "8.8.8.8:53"
|
config.DNSServer.BackupServer = "8.8.8.8:53"
|
||||||
}
|
}
|
||||||
@@ -457,8 +546,9 @@ func GenerateExampleConfig(filename string) error {
|
|||||||
// Generate server configuration
|
// Generate server configuration
|
||||||
config = Config{
|
config = Config{
|
||||||
InstanceID: "teleport-server-01",
|
InstanceID: "teleport-server-01",
|
||||||
ListenAddress: ":8080",
|
ListenAddress: "127.0.0.1:8080",
|
||||||
RemoteAddress: "",
|
RemoteAddress: "",
|
||||||
|
BindAddress: DefaultBindAddress,
|
||||||
Ports: []PortRule{
|
Ports: []PortRule{
|
||||||
{LocalPort: 80, RemotePort: 80, Protocol: "tcp", TargetHost: "localhost"},
|
{LocalPort: 80, RemotePort: 80, Protocol: "tcp", TargetHost: "localhost"},
|
||||||
},
|
},
|
||||||
@@ -475,6 +565,7 @@ func GenerateExampleConfig(filename string) error {
|
|||||||
},
|
},
|
||||||
DNSServer: DNSServerConfig{
|
DNSServer: DNSServerConfig{
|
||||||
ListenPort: 5353,
|
ListenPort: 5353,
|
||||||
|
BindAddress: DefaultBindAddress,
|
||||||
BackupServer: "8.8.8.8:53",
|
BackupServer: "8.8.8.8:53",
|
||||||
CustomRecords: []DNSRecord{},
|
CustomRecords: []DNSRecord{},
|
||||||
},
|
},
|
||||||
@@ -485,6 +576,7 @@ func GenerateExampleConfig(filename string) error {
|
|||||||
InstanceID: "teleport-client-01",
|
InstanceID: "teleport-client-01",
|
||||||
ListenAddress: "",
|
ListenAddress: "",
|
||||||
RemoteAddress: "localhost:8080",
|
RemoteAddress: "localhost:8080",
|
||||||
|
BindAddress: DefaultBindAddress,
|
||||||
Ports: []PortRule{
|
Ports: []PortRule{
|
||||||
{LocalPort: 8080, RemotePort: 80, Protocol: "tcp", TargetHost: ""},
|
{LocalPort: 8080, RemotePort: 80, Protocol: "tcp", TargetHost: ""},
|
||||||
},
|
},
|
||||||
@@ -501,6 +593,7 @@ func GenerateExampleConfig(filename string) error {
|
|||||||
},
|
},
|
||||||
DNSServer: DNSServerConfig{
|
DNSServer: DNSServerConfig{
|
||||||
ListenPort: 5353,
|
ListenPort: 5353,
|
||||||
|
BindAddress: DefaultBindAddress,
|
||||||
BackupServer: "8.8.8.8:53",
|
BackupServer: "8.8.8.8:53",
|
||||||
CustomRecords: []DNSRecord{
|
CustomRecords: []DNSRecord{
|
||||||
{Name: "app.local", Type: "A", Value: "127.0.0.1", TTL: 300},
|
{Name: "app.local", Type: "A", Value: "127.0.0.1", TTL: 300},
|
||||||
@@ -514,24 +607,33 @@ func GenerateExampleConfig(filename string) error {
|
|||||||
return fmt.Errorf("failed to marshal config: %v", err)
|
return fmt.Errorf("failed to marshal config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(filename, data, 0644)
|
err = os.WriteFile(filename, data, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write config file: %v", err)
|
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("Generated example configuration: %s\n", filename)
|
||||||
fmt.Printf("Edit the configuration file and run: ./teleport -config %s\n", filename)
|
fmt.Printf("Edit the configuration file and run: ./teleport -config %s\n", filename)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateStrongEncryptionKey generates a cryptographically secure encryption key
|
// generateStrongEncryptionKey generates a raw 256-bit key (raw: + 64 hex chars).
|
||||||
func generateStrongEncryptionKey() (string, error) {
|
func generateStrongEncryptionKey() (string, error) {
|
||||||
// Generate 32 random bytes (256 bits) for a strong encryption key
|
return encryption.GenerateRawKey()
|
||||||
bytes := make([]byte, 32)
|
}
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate random key: %v", err)
|
// ApplyTCPKeepAlive enables TCP keep-alive when enabled is true.
|
||||||
}
|
func ApplyTCPKeepAlive(conn net.Conn, enabled bool) {
|
||||||
|
if conn == nil || !enabled {
|
||||||
// Convert to hexadecimal string for easy copying
|
return
|
||||||
return hex.EncodeToString(bytes), nil
|
}
|
||||||
|
tcp, ok := conn.(*net.TCPConn)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = tcp.SetKeepAlive(true)
|
||||||
|
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"teleport/pkg/encryption"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadConfig(t *testing.T) {
|
func TestLoadConfig(t *testing.T) {
|
||||||
@@ -135,6 +138,14 @@ encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
|||||||
if config.DNSServer.BackupServer != "8.8.8.8:53" {
|
if config.DNSServer.BackupServer != "8.8.8.8:53" {
|
||||||
t.Errorf("Expected default backup server '8.8.8.8:53', got '%s'", config.DNSServer.BackupServer)
|
t.Errorf("Expected default backup server '8.8.8.8:53', got '%s'", config.DNSServer.BackupServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.BindAddress != DefaultBindAddress {
|
||||||
|
t.Errorf("Expected default BindAddress %q, got %q", DefaultBindAddress, config.BindAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DNSServer.BindAddress != DefaultBindAddress {
|
||||||
|
t.Errorf("Expected default DNS BindAddress %q, got %q", DefaultBindAddress, config.DNSServer.BindAddress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectMode(t *testing.T) {
|
func TestDetectMode(t *testing.T) {
|
||||||
@@ -361,3 +372,276 @@ encryption_key: test-key
|
|||||||
t.Errorf("Expected error message to mention 'protocol://', got: %v", err)
|
t.Errorf("Expected error message to mention 'protocol://', got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultLocalListenAddrIsLoopback(t *testing.T) {
|
||||||
|
if got := LocalListenAddr("", 9000); got != "127.0.0.1:9000" {
|
||||||
|
t.Fatalf("empty host: got %q want 127.0.0.1:9000", got)
|
||||||
|
}
|
||||||
|
if got := LocalListenAddr("127.0.0.1", 2222); got != "127.0.0.1:2222" {
|
||||||
|
t.Fatalf("loopback host: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
tcpRule := PortRule{LocalPort: 8080, RemotePort: 80, Protocol: "tcp"}
|
||||||
|
udpRule := PortRule{LocalPort: 5353, RemotePort: 53, Protocol: "udp"}
|
||||||
|
if got := cfg.ClientListenAddr(tcpRule); got != "127.0.0.1:8080" {
|
||||||
|
t.Fatalf("default TCP listen: got %q want 127.0.0.1:8080", got)
|
||||||
|
}
|
||||||
|
if got := cfg.ClientListenAddr(udpRule); got != "127.0.0.1:5353" {
|
||||||
|
t.Fatalf("default UDP listen: got %q want 127.0.0.1:5353", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsCfg := DNSServerConfig{ListenPort: 5353}
|
||||||
|
if got := dnsCfg.ListenAddr(); got != "127.0.0.1:5353" {
|
||||||
|
t.Fatalf("default DNS listen: got %q want 127.0.0.1:5353", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExplicitBindAddressOverride(t *testing.T) {
|
||||||
|
if got := LocalListenAddr("0.0.0.0", 9000); got != "0.0.0.0:9000" {
|
||||||
|
t.Fatalf("0.0.0.0 override: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{BindAddress: "0.0.0.0"}
|
||||||
|
rule := PortRule{LocalPort: 2222, RemotePort: 22, Protocol: "tcp"}
|
||||||
|
if got := cfg.ClientListenAddr(rule); got != "0.0.0.0:2222" {
|
||||||
|
t.Fatalf("global bind_address 0.0.0.0: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.BindAddress = "10.0.0.5"
|
||||||
|
if got := cfg.ClientListenAddr(rule); got != "10.0.0.5:2222" {
|
||||||
|
t.Fatalf("per-rule host should win: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsCfg := DNSServerConfig{ListenPort: 5353, BindAddress: "0.0.0.0"}
|
||||||
|
if got := dnsCfg.ListenAddr(); got != "0.0.0.0:5353" {
|
||||||
|
t.Fatalf("DNS bind_address 0.0.0.0: got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalListenAddrBinds(t *testing.T) {
|
||||||
|
ln, err := net.Listen("tcp", LocalListenAddr("", 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen default: %v", err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
ip := ln.Addr().(*net.TCPAddr).IP
|
||||||
|
if !ip.IsLoopback() {
|
||||||
|
t.Fatalf("default TCP bind is not loopback: %v", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
udpAddr, err := net.ResolveUDPAddr("udp", LocalListenAddr("", 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve default udp: %v", err)
|
||||||
|
}
|
||||||
|
uc, err := net.ListenUDP("udp", udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen default udp: %v", err)
|
||||||
|
}
|
||||||
|
defer uc.Close()
|
||||||
|
if !uc.LocalAddr().(*net.UDPAddr).IP.IsLoopback() {
|
||||||
|
t.Fatalf("default UDP bind is not loopback: %v", uc.LocalAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
all, err := net.Listen("tcp", LocalListenAddr("0.0.0.0", 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen 0.0.0.0: %v", err)
|
||||||
|
}
|
||||||
|
defer all.Close()
|
||||||
|
if !all.Addr().(*net.TCPAddr).IP.IsUnspecified() {
|
||||||
|
t.Fatalf("explicit 0.0.0.0 bind is not unspecified: %v", all.Addr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortRuleBindHostFormat(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config string
|
||||||
|
expected PortRule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "client with loopback bind",
|
||||||
|
config: `
|
||||||
|
instance_id: test
|
||||||
|
remote_address: localhost:8080
|
||||||
|
ports:
|
||||||
|
- tcp://22:127.0.0.1:2222
|
||||||
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
||||||
|
`,
|
||||||
|
expected: PortRule{LocalPort: 2222, RemotePort: 22, Protocol: "tcp", BindAddress: "127.0.0.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "client with all-interfaces bind",
|
||||||
|
config: `
|
||||||
|
instance_id: test
|
||||||
|
remote_address: localhost:8080
|
||||||
|
ports:
|
||||||
|
- tcp://22:0.0.0.0:2222
|
||||||
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
||||||
|
`,
|
||||||
|
expected: PortRule{LocalPort: 2222, RemotePort: 22, Protocol: "tcp", BindAddress: "0.0.0.0"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "client two-part still defaults bind via config",
|
||||||
|
config: `
|
||||||
|
instance_id: test
|
||||||
|
remote_address: localhost:8080
|
||||||
|
ports:
|
||||||
|
- tcp://80:8080
|
||||||
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
||||||
|
`,
|
||||||
|
expected: PortRule{LocalPort: 8080, RemotePort: 80, Protocol: "tcp", BindAddress: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tempDir, "test-config.yaml")
|
||||||
|
if err := os.WriteFile(configFile, []byte(tt.config), 0644); err != nil {
|
||||||
|
t.Fatalf("write config: %v", err)
|
||||||
|
}
|
||||||
|
cfg, err := LoadConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
if len(cfg.Ports) != 1 {
|
||||||
|
t.Fatalf("expected 1 port, got %d", len(cfg.Ports))
|
||||||
|
}
|
||||||
|
port := cfg.Ports[0]
|
||||||
|
if port.LocalPort != tt.expected.LocalPort || port.RemotePort != tt.expected.RemotePort ||
|
||||||
|
port.Protocol != tt.expected.Protocol || port.BindAddress != tt.expected.BindAddress {
|
||||||
|
t.Errorf("got %+v want %+v", port, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigGlobalBindAddress(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tempDir, "cfg.yaml")
|
||||||
|
content := `
|
||||||
|
instance_id: test
|
||||||
|
remote_address: localhost:8080
|
||||||
|
bind_address: 0.0.0.0
|
||||||
|
ports:
|
||||||
|
- tcp://80:8080
|
||||||
|
encryption_key: a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03
|
||||||
|
dns_server:
|
||||||
|
enabled: true
|
||||||
|
listen_port: 5353
|
||||||
|
bind_address: 0.0.0.0
|
||||||
|
backup_server: 8.8.8.8:53
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err := LoadConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.BindAddress != "0.0.0.0" {
|
||||||
|
t.Fatalf("BindAddress got %q", cfg.BindAddress)
|
||||||
|
}
|
||||||
|
if cfg.DNSServer.BindAddress != "0.0.0.0" {
|
||||||
|
t.Fatalf("DNS BindAddress got %q", cfg.DNSServer.BindAddress)
|
||||||
|
}
|
||||||
|
if got := cfg.ClientListenAddr(cfg.Ports[0]); got != "0.0.0.0:8080" {
|
||||||
|
t.Fatalf("client listen got %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.DNSServer.ListenAddr(); got != "0.0.0.0:5353" {
|
||||||
|
t.Fatalf("dns listen got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateExampleConfigUsesLoopback(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
serverFile := filepath.Join(tempDir, "server.yaml")
|
||||||
|
if err := GenerateExampleConfig(serverFile); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(serverFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body := string(data)
|
||||||
|
if strings.Contains(body, "listen_address: :8080") || strings.Contains(body, "listen_address: :9000") {
|
||||||
|
t.Fatalf("generated server config still documents all-interfaces listen: %s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "127.0.0.1:8080") {
|
||||||
|
t.Fatalf("generated server config missing loopback listen_address: %s", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientFile := filepath.Join(tempDir, "client.yaml")
|
||||||
|
if err := GenerateExampleConfig(clientFile); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err := LoadConfig(clientFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload generated client: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.BindAddress != DefaultBindAddress {
|
||||||
|
t.Fatalf("generated client bind_address %q", cfg.BindAddress)
|
||||||
|
}
|
||||||
|
if got := cfg.ClientListenAddr(cfg.Ports[0]); !strings.HasPrefix(got, "127.0.0.1:") {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-3
@@ -1,7 +1,6 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,9 +17,10 @@ func StartDNSServer(cfg *config.Config) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listenAddr := cfg.DNSServer.ListenAddr()
|
||||||
// Create DNS server
|
// Create DNS server
|
||||||
server := &dns.Server{
|
server := &dns.Server{
|
||||||
Addr: fmt.Sprintf(":%d", cfg.DNSServer.ListenPort),
|
Addr: listenAddr,
|
||||||
Net: "udp",
|
Net: "udp",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,10 @@ func StartDNSServer(cfg *config.Config) {
|
|||||||
handleDNSQuery(w, r, cfg)
|
handleDNSQuery(w, r, cfg)
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.WithField("port", cfg.DNSServer.ListenPort).Info("DNS server started")
|
logger.WithFields(map[string]interface{}{
|
||||||
|
"addr": listenAddr,
|
||||||
|
"port": cfg.DNSServer.ListenPort,
|
||||||
|
}).Info("DNS server started")
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
if err := server.ListenAndServe(); err != nil {
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,22 +26,69 @@ const (
|
|||||||
// Replay protection parameters
|
// Replay protection parameters
|
||||||
MaxPacketAge = 5 * time.Minute // Maximum age for UDP packets
|
MaxPacketAge = 5 * time.Minute // Maximum age for UDP packets
|
||||||
NonceWindow = 1000 // Number of nonces to track for replay protection
|
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 {
|
func DeriveKey(password string) []byte {
|
||||||
// Use a deterministic salt derived from the password hash for consistent key derivation
|
if v, ok := deriveCache.Load(password); ok {
|
||||||
// This ensures the same password always produces the same key while avoiding rainbow tables
|
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 := sha256.New()
|
||||||
hasher.Write([]byte(password))
|
hasher.Write([]byte(password))
|
||||||
passwordHash := hasher.Sum(nil)
|
passwordHash := hasher.Sum(nil)
|
||||||
|
|
||||||
// Create a deterministic salt from the password hash
|
|
||||||
salt := make([]byte, PBKDF2SaltLength)
|
salt := make([]byte, PBKDF2SaltLength)
|
||||||
copy(salt, passwordHash[:PBKDF2SaltLength])
|
copy(salt, passwordHash[:PBKDF2SaltLength])
|
||||||
|
|
||||||
key := pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, PBKDF2KeyLength, sha256.New)
|
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
|
// 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
|
// ValidateEncryptionKey validates that an encryption key meets security requirements
|
||||||
func ValidateEncryptionKey(key string) error {
|
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 {
|
if len(key) < 32 {
|
||||||
return fmt.Errorf("encryption key must be at least 32 characters long")
|
return fmt.Errorf("encryption key must be at least 32 characters long")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package encryption
|
package encryption
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -240,3 +242,95 @@ func TestConstantTimeCompare(t *testing.T) {
|
|||||||
t.Error("Empty slices should compare equal")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxFrameSize is the maximum AES-GCM ciphertext length accepted on a framed
|
||||||
|
// TCP tunnel (uint32 length prefix + ciphertext).
|
||||||
|
const MaxFrameSize = 1024 * 1024 // 1 MiB
|
||||||
|
|
||||||
|
// WriteEncryptedFrame encrypts plaintext with AES-GCM (EncryptData) and writes
|
||||||
|
// a uint32 big-endian length prefix followed by the ciphertext.
|
||||||
|
func WriteEncryptedFrame(w io.Writer, plaintext, key []byte) error {
|
||||||
|
ciphertext, err := EncryptData(plaintext, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(ciphertext) == 0 {
|
||||||
|
return fmt.Errorf("encrypted data cannot be empty")
|
||||||
|
}
|
||||||
|
if len(ciphertext) > MaxFrameSize {
|
||||||
|
return fmt.Errorf("frame too large: %d bytes (max %d)", len(ciphertext), MaxFrameSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var length [4]byte
|
||||||
|
binary.BigEndian.PutUint32(length[:], uint32(len(ciphertext)))
|
||||||
|
if err := writeFull(w, length[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeFull(w, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadEncryptedFrame reads a uint32 big-endian length, caps it, reads the
|
||||||
|
// ciphertext, and decrypts it with AES-GCM (DecryptData).
|
||||||
|
func ReadEncryptedFrame(r io.Reader, key []byte) ([]byte, error) {
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if length == 0 {
|
||||||
|
return nil, fmt.Errorf("frame length cannot be zero")
|
||||||
|
}
|
||||||
|
if length > MaxFrameSize {
|
||||||
|
return nil, fmt.Errorf("frame too large: %d bytes (max %d)", length, MaxFrameSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, ciphertext); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read frame data: %v", err)
|
||||||
|
}
|
||||||
|
return DecryptData(ciphertext, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFull(w io.Writer, data []byte) error {
|
||||||
|
for len(data) > 0 {
|
||||||
|
n, err := w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return io.ErrShortWrite
|
||||||
|
}
|
||||||
|
data = data[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testKey(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
return DeriveKey("test-encryption-key-for-tcp-frames")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedFrameRoundTrip(t *testing.T) {
|
||||||
|
key := testKey(t)
|
||||||
|
original := []byte("Hello, framed TCP tunnel!")
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := WriteEncryptedFrame(&buf, original, key); err != nil {
|
||||||
|
t.Fatalf("WriteEncryptedFrame failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ReadEncryptedFrame(&buf, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadEncryptedFrame failed: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, original) {
|
||||||
|
t.Errorf("round trip mismatch: got %q want %q", got, original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedFrameMultipleChunks(t *testing.T) {
|
||||||
|
key := testKey(t)
|
||||||
|
chunks := [][]byte{
|
||||||
|
[]byte("chunk-one"),
|
||||||
|
[]byte("chunk-two-is-longer"),
|
||||||
|
[]byte{0x00, 0x01, 0xff},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, c := range chunks {
|
||||||
|
if err := WriteEncryptedFrame(&buf, c, key); err != nil {
|
||||||
|
t.Fatalf("WriteEncryptedFrame failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, want := range chunks {
|
||||||
|
got, err := ReadEncryptedFrame(&buf, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadEncryptedFrame chunk %d failed: %v", i, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("chunk %d mismatch: got %q want %q", i, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedFrameRejectsZeroLength(t *testing.T) {
|
||||||
|
key := testKey(t)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := binary.Write(&buf, binary.BigEndian, uint32(0)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ReadEncryptedFrame(&buf, key); err == nil {
|
||||||
|
t.Fatal("expected error for zero-length frame")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedFrameRejectsOversizeLength(t *testing.T) {
|
||||||
|
key := testKey(t)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := binary.Write(&buf, binary.BigEndian, uint32(MaxFrameSize+1)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ReadEncryptedFrame(&buf, key); err == nil {
|
||||||
|
t.Fatal("expected error for oversize frame length")
|
||||||
|
} else if !strings.Contains(err.Error(), "too large") {
|
||||||
|
t.Errorf("expected too-large error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedFrameWrongKey(t *testing.T) {
|
||||||
|
key1 := DeriveKey("tcp-frame-key-1")
|
||||||
|
key2 := DeriveKey("tcp-frame-key-2")
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := WriteEncryptedFrame(&buf, []byte("secret"), key1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ReadEncryptedFrame(&buf, key2); err == nil {
|
||||||
|
t.Fatal("decrypting with the wrong key should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlaintextNotVisibleOnFramedTunnel(t *testing.T) {
|
||||||
|
key := testKey(t)
|
||||||
|
req := []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
||||||
|
|
||||||
|
var wire bytes.Buffer
|
||||||
|
if err := WriteEncryptedFrame(&wire, req, key); err != nil {
|
||||||
|
t.Fatalf("WriteEncryptedFrame failed: %v", err)
|
||||||
|
}
|
||||||
|
sniffed := wire.Bytes()
|
||||||
|
|
||||||
|
if bytes.Contains(sniffed, []byte("GET /")) {
|
||||||
|
t.Fatal("sniffer saw HTTP request line on the tunnel")
|
||||||
|
}
|
||||||
|
if bytes.Contains(sniffed, []byte("Host:")) {
|
||||||
|
t.Fatal("sniffer saw HTTP Host header on the tunnel")
|
||||||
|
}
|
||||||
|
if bytes.Contains(sniffed, []byte("example.com")) {
|
||||||
|
t.Fatal("sniffer saw HTTP hostname on the tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length prefix is 4 bytes; ciphertext must be longer than plaintext.
|
||||||
|
if len(sniffed) < 4+len(req) {
|
||||||
|
t.Fatalf("framed ciphertext too short: %d", len(sniffed))
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ReadEncryptedFrame(bytes.NewReader(sniffed), key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt failed: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, req) {
|
||||||
|
t.Errorf("decrypted HTTP mismatch: got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadEncryptedFrameEOF(t *testing.T) {
|
||||||
|
key := testKey(t)
|
||||||
|
if _, err := ReadEncryptedFrame(bytes.NewReader(nil), key); err != io.EOF {
|
||||||
|
t.Errorf("expected io.EOF, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-18
@@ -27,28 +27,24 @@ type Config struct {
|
|||||||
Compress bool `yaml:"compress"` // compress backup files
|
Compress bool `yaml:"compress"` // compress backup files
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the global logger with the given configuration
|
func newLogger(config Config) (*logrus.Logger, error) {
|
||||||
func Init(config Config) error {
|
l := logrus.New()
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
Log = logrus.New()
|
|
||||||
|
|
||||||
// Set log level
|
// Set log level
|
||||||
level, err := logrus.ParseLevel(config.Level)
|
level, err := logrus.ParseLevel(config.Level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
level = logrus.InfoLevel
|
level = logrus.InfoLevel
|
||||||
}
|
}
|
||||||
Log.SetLevel(level)
|
l.SetLevel(level)
|
||||||
|
|
||||||
// Set log format with sanitization
|
// Set log format with sanitization
|
||||||
switch config.Format {
|
switch config.Format {
|
||||||
case "json":
|
case "json":
|
||||||
Log.SetFormatter(&SanitizedJSONFormatter{
|
l.SetFormatter(&SanitizedJSONFormatter{
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
TimestampFormat: "2006-01-02 15:04:05",
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
Log.SetFormatter(&SanitizedTextFormatter{
|
l.SetFormatter(&SanitizedTextFormatter{
|
||||||
FullTimestamp: true,
|
FullTimestamp: true,
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
TimestampFormat: "2006-01-02 15:04:05",
|
||||||
})
|
})
|
||||||
@@ -59,36 +55,49 @@ func Init(config Config) error {
|
|||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
dir := filepath.Dir(config.File)
|
dir := filepath.Dir(config.File)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open log file with secure permissions (owner read/write only)
|
// Open log file with secure permissions (owner read/write only)
|
||||||
file, err := os.OpenFile(config.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
file, err := os.OpenFile(config.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set output to both file and stdout
|
// Set output to both file and stdout
|
||||||
Log.SetOutput(io.MultiWriter(file, os.Stdout))
|
l.SetOutput(io.MultiWriter(file, os.Stdout))
|
||||||
} else {
|
} else {
|
||||||
Log.SetOutput(os.Stdout)
|
l.SetOutput(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the global logger with the given configuration
|
||||||
|
func Init(config Config) error {
|
||||||
|
l, err := newLogger(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
Log = l
|
||||||
|
mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogger returns the global logger instance
|
// GetLogger returns the global logger instance
|
||||||
func GetLogger() *logrus.Logger {
|
func GetLogger() *logrus.Logger {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
if Log != nil {
|
l := Log
|
||||||
mu.RUnlock()
|
|
||||||
return Log
|
|
||||||
}
|
|
||||||
mu.RUnlock()
|
mu.RUnlock()
|
||||||
|
if l != nil {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize with default config if not already initialized
|
// Initialize with default config if not already initialized
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
Init(Config{
|
_ = Init(Config{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
Format: "text",
|
Format: "text",
|
||||||
File: "",
|
File: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user