Skip to content

Usage

Pass a single LOCAL HOST REMOTE triple, or one or more LOCAL:HOST:REMOTE mappings. Each accepted client opens a new connection to that mapping's remote endpoint and relays bytes both ways. Shared flags apply to every mapping.

IPv6 remotes use brackets: 8080:[::1]:80.

CLI flags

Flag Default Description
--bind 127.0.0.1 Local address to listen on
--timeout 10 Remote connect timeout in seconds
--max-connections unlimited Max concurrent sessions per listener
--allow off Repeatable source IP or CIDR allowlist
--idle-timeout off Close a session after this many idle seconds
--verbose off Log debug detail
--quiet off Log warnings and errors only
--version Print the package version and exit

--verbose and --quiet cannot be combined. --allow is most useful with --bind 0.0.0.0. --timeout is the remote connect bound; it is not the idle timer.

port-forwarder 8080 remote.example.com 80
port-forwarder 8080:remote.example.com:80 5432:db.internal:5432
port-forwarder 5432 db.internal 5432 --bind 0.0.0.0 --allow 10.0.0.0/8
port-forwarder 8080 remote.example.com 80 --max-connections 8 --idle-timeout 60
port-forwarder 8080 remote.example.com 80 --timeout 2.5
port-forwarder --version

Listen ports and remote ports must be integers from 1 to 65535.

The process runs until it is interrupted. Ctrl+C or SIGTERM closes the listen sockets, waits for in-flight relays, and exits 0. A bind or listen failure prints an error to stderr and exits 1.

When one side half-closes its write stream, the other direction stays open so the reply can still arrive.

Library

from lupaxa.port_forwarder import PortForwarder

forwarder = PortForwarder(8080, "remote.example.com", 80)
forwarder.start()

start() blocks. Call stop() from another thread to close the listener and in-flight sockets. Pass local_port=0 to ask the OS for an ephemeral port, then read bound_port after on_ready fires:

from threading import Event, Thread

from lupaxa.port_forwarder import PortForwarder

ready = Event()
with PortForwarder(0, "127.0.0.1", 9000, connect_timeout=2.5) as forwarder:
    thread = Thread(target=forwarder.start, kwargs={"on_ready": ready.set}, daemon=True)
    thread.start()
    ready.wait()
    print(forwarder.bound_port)

Several mappings can share a PortForwarderGroup. Leaving a with block calls stop().