/fd3-proxy

Lazy-load TCP services through systemd socket activation with automatic resource management

Primary LanguageRustMIT LicenseMIT

fd3-proxy

A TCP proxy that forwards connections from systemd socket activation to a backend service, with optional inactivity timeout for resource management.

Overview

fd3-proxy is designed to work with systemd socket activation, receiving connections on file descriptor 3 and forwarding them to a backend service. This is particularly useful for resource-intensive services that you want to start on-demand and automatically shut down when not in use.

Features

  • Systemd Socket Activation: Receives connections via systemd socket activation (file descriptor 3)
  • Automatic Port Assignment: Can auto-assign ports or use specified ports for the backend
  • Bidirectional TCP Forwarding: Full-duplex connection forwarding between client and backend
  • Inactivity Timeout: Automatically shuts down after a period of inactivity to save resources
  • Process Lifecycle Management: Monitors the backend process and shuts down when it exits
  • Comprehensive Logging: Detailed logging with configurable levels via RUST_LOG

Installation

From Source

git clone <repository-url>
cd fd3-proxy
cargo build --release
sudo cp target/release/fd3-proxy /usr/local/bin/

The binary will be installed to /usr/local/bin/fd3-proxy.

Usage

Note: fd3-proxy is designed to work with systemd socket activation and expects file descriptor 3 to be a valid socket. It cannot be run directly from the command line for testing.

fd3-proxy --forward-tcp <host:port> [--timeout <seconds>] -- <command> [args...]

Arguments

  • --forward-tcp <host:port>: Forward address for the backend service
    • Use 127.0.0.1:0 for auto-assigned port on localhost
    • Use 127.0.0.1:8080 for a specific port
    • Use 0.0.0.0:0 for auto-assigned port on all interfaces
  • --timeout <seconds>: Inactivity timeout in seconds (default: 0 = no timeout)
  • -- <command> [args...]: Command and arguments to execute as the backend service

Port Substitution

The {port} placeholder in the command arguments will be replaced with the actual assigned port number. This allows the backend service to know which port to bind to.

Systemd Integration

Here's a complete example using whisper.cpp service that auto-shuts down after 2 minutes of inactivity:

Socket Unit (whisper-service.socket)

[Unit]
Description=Socket for lazy whisper.cpp service
PartOf=whisper-service.service

[Socket]
ListenStream=127.0.0.1:58080
Accept=no
RemoveOnStop=yes

[Install]
WantedBy=default.target

Service Unit (whisper-service.service)

[Unit]
Description=fd3-proxy for whisper.cpp
Requires=whisper-service.socket
After=network.target

[Service]
Environment=RUST_LOG=debug
ExecStart=/usr/local/bin/fd3-proxy \
  --forward-tcp 127.0.0.1:0 \
  --timeout 120 \
  -- \
  /path/to/whisper.cpp/build/bin/whisper-server \
    -m /path/to/whisper.cpp/models/ggml-large-v3.bin \
    --port {port}
StandardError=journal
Restart=on-failure

Setup and Management

Install and Enable the Service

# Place the unit files in ~/.config/systemd/user/ for user services
# or /etc/systemd/system/ for system services

# Reload systemd configuration
systemctl --user daemon-reload

# Enable and start the socket
systemctl --user enable --now whisper-service.socket

Monitor the Service

# Follow the service logs
journalctl --user -u whisper-service -f

# Check socket status
systemctl --user status whisper-service.socket

# Check service status
systemctl --user status whisper-service.service

Test the Service

# Connect to the service (this will trigger activation)
curl http://127.0.0.1:58080/

# Or use any HTTP client to connect to port 58080

Logging

The proxy uses the log crate with env_logger. Control logging levels with the RUST_LOG environment variable in your systemd service unit:

[Service]
Environment=RUST_LOG=debug  # Show debug and above
# Environment=RUST_LOG=info   # Show info and above (default)
# Environment=RUST_LOG=trace  # Show all logs
# Environment=RUST_LOG=error  # Show only errors

How It Works

  1. Socket Activation: Systemd passes the listening socket as file descriptor 3
  2. Backend Startup: The proxy starts the specified backend command with port substitution
  3. Port Assignment: Either auto-assigns a port or uses the specified port for the backend
  4. Connection Forwarding: Accepts connections on the systemd socket and forwards them to the backend
  5. Activity Tracking: Tracks connection activity for inactivity timeout
  6. Lifecycle Management: Shuts down when:
    • The backend process exits
    • Inactivity timeout is reached (if configured)

Architecture

[Client] → [Systemd Socket] → [fd3-proxy] → [Backend Service]
                                    ↓
                            [Inactivity Monitor]
                                    ↓
                            [Process Monitor]

Dependencies

  • clap: Command-line argument parsing
  • log: Logging facade
  • env_logger: Simple logger implementation

Error Handling

  • Backend Connection Failures: Logged as errors, proxy continues accepting new connections
  • Backend Process Exit: Proxy shuts down with the same exit code
  • Inactivity Timeout: Proxy shuts down gracefully with exit code 0
  • Socket Errors: Logged and handled gracefully where possible

Use Cases

  • Resource-Intensive Services: Start heavy services only when needed
  • Development Environments: Automatically manage service lifecycles
  • Microservices: On-demand service activation in containerized environments
  • Legacy Service Integration: Add socket activation to services that don't support it natively

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Troubleshooting

"IO Safety violation: owned file descriptor already closed"

This error occurs when running outside of systemd socket activation. The proxy expects file descriptor 3 to be a valid socket provided by systemd. You cannot test fd3-proxy directly from the command line.

Service Not Starting

  1. Check the service status:

    systemctl --user status whisper-service.service
  2. Check the logs for detailed error information:

    journalctl --user -u whisper-service -f
  3. Verify the socket is listening:

    systemctl --user status whisper-service.socket
    ss -tlnp | grep 58080

Backend Service Not Starting

Check the logs with Environment=RUST_LOG=debug in your service unit to see detailed information about the backend startup process and port assignment.

Connection Refused

  1. Ensure the backend service is binding to the correct port. The {port} substitution should match what your service expects.

  2. Check if the backend process is actually running:

    journalctl --user -u whisper-service -f
  3. Test the socket activation:

    # This should trigger service startup
    curl http://127.0.0.1:58080/

Service Keeps Restarting

If you see the service constantly restarting, check:

  1. The backend command path is correct
  2. The backend service supports the --port argument
  3. The model file path exists (in the whisper example)
  4. The user has permission to execute the backend service

Inactivity Timeout Not Working

Ensure you have the --timeout parameter set in your ExecStart command. The timeout only starts counting after the first connection is made.