A TCP proxy that forwards connections from systemd socket activation to a backend service, with optional inactivity timeout for resource management.
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.
- 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
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.
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...]--forward-tcp <host:port>: Forward address for the backend service- Use
127.0.0.1:0for auto-assigned port on localhost - Use
127.0.0.1:8080for a specific port - Use
0.0.0.0:0for auto-assigned port on all interfaces
- Use
--timeout <seconds>: Inactivity timeout in seconds (default: 0 = no timeout)-- <command> [args...]: Command and arguments to execute as the backend service
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.
Here's a complete example using whisper.cpp service that auto-shuts down after 2 minutes of inactivity:
[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[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# 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# 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# Connect to the service (this will trigger activation)
curl http://127.0.0.1:58080/
# Or use any HTTP client to connect to port 58080The 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- Socket Activation: Systemd passes the listening socket as file descriptor 3
- Backend Startup: The proxy starts the specified backend command with port substitution
- Port Assignment: Either auto-assigns a port or uses the specified port for the backend
- Connection Forwarding: Accepts connections on the systemd socket and forwards them to the backend
- Activity Tracking: Tracks connection activity for inactivity timeout
- Lifecycle Management: Shuts down when:
- The backend process exits
- Inactivity timeout is reached (if configured)
[Client] → [Systemd Socket] → [fd3-proxy] → [Backend Service]
↓
[Inactivity Monitor]
↓
[Process Monitor]
clap: Command-line argument parsinglog: Logging facadeenv_logger: Simple logger implementation
- 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
- 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
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
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.
-
Check the service status:
systemctl --user status whisper-service.service
-
Check the logs for detailed error information:
journalctl --user -u whisper-service -f
-
Verify the socket is listening:
systemctl --user status whisper-service.socket ss -tlnp | grep 58080
Check the logs with Environment=RUST_LOG=debug in your service unit to see detailed information about the backend startup process and port assignment.
-
Ensure the backend service is binding to the correct port. The
{port}substitution should match what your service expects. -
Check if the backend process is actually running:
journalctl --user -u whisper-service -f
-
Test the socket activation:
# This should trigger service startup curl http://127.0.0.1:58080/
If you see the service constantly restarting, check:
- The backend command path is correct
- The backend service supports the
--portargument - The model file path exists (in the whisper example)
- The user has permission to execute the backend service
Ensure you have the --timeout parameter set in your ExecStart command. The timeout only starts counting after the first connection is made.