Metadata-Version: 2.4
Name: aio_network_forward
Version: 0.1.3
Summary: Lightweight async TCP/UDP port forwarding utility built on asyncio
Author-email: yinkaisheng <yinkaisheng@foxmail.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/yinkaisheng/aio-network-forward
Keywords: asyncio,socket,network,forward
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aio-sockets
Dynamic: license-file

# aio-network-forward

💡 Overview

aio-network-forward is a lightweight and user-friendly library for creating asynchronous TCP and UDP port forwards in Python. Built on top of `asyncio` and `aio-sockets`, it exposes a very small API for starting and stopping local forwarding endpoints without dealing with the low-level socket lifecycle yourself.

This library is useful when you want to proxy a local port to another TCP or UDP service, build small tunneling utilities, or embed simple forwarding behavior directly into your own async applications.

✨ Features

Simple Port Forwarding API: Start or stop TCP and UDP forwarding with just a few coroutine calls.

Automatic Port Fallback: If the requested local port is already in use, the library can fall back to a random available port.

Bidirectional Relay: TCP traffic is proxied in both directions, and UDP replies are relayed back to the original sender automatically.

IPv4 and IPv6 Support: Address family is selected automatically from the target address.

Embeddable Design: The library is easy to integrate into existing `asyncio` applications and lets you customize logging and listening addresses.

Multiple Backends and Load Balancing: Register several remote targets on the same local port; new connections are distributed across backends with round-robin, and failed backends are skipped automatically.

Backend Failover: If a TCP backend cannot be reached (connection refused, timeout, etc.), the library tries the next backend in the list before closing the client connection.

📦 Installation

Install from PyPI:

```shell
pip install aio-network-forward aio-sockets
```

Minimum supported version: Python 3.8

## Usage

### Basic Forward Example

The example below shows both TCP and UDP usage. Replace the startup line depending on which protocol you want to forward.

```python
import asyncio
import aio_sockets as aio
import aio_network_forward as aionf


async def main():
    forward_to = ("127.0.0.1", 8686)

    # TCP
    local_port = await aionf.start_forward_tcp(forward_to, 8080)

    # UDP
    # local_port = await aionf.start_forward_udp(forward_to, 8080)

    print(f"forwarding started on 0.0.0.0:{local_port} -> {forward_to[0]}:{forward_to[1]}")
    await aio.wait_all_tasks_done()


asyncio.run(main())
```

### Bind to a Random Local Port

If the preferred local port is unavailable, the library will automatically retry with a random available port. You can also pass `0` directly if you always want the OS to choose one.

```python
import asyncio
import aio_sockets as aio
import aio_network_forward as aionf


async def main():
    forward_to = ("127.0.0.1", 8686)
    local_port = await aionf.start_forward_tcp(forward_to, 0)
    print(f"allocated local port: {local_port}")
    await aio.wait_all_tasks_done()


asyncio.run(main())
```

### Multiple Backends on One Local Port

If you call `start_forward_tcp()` or `start_forward_udp()` again with the same `local_port`, the new `forward_to_addr` is appended to the existing backend list instead of creating a second listener. Incoming connections (TCP) or new UDP clients are assigned to backends in round-robin order.

Use the port returned by the first call as `local_port` for subsequent calls. Passing `0` again creates a separate listener rather than appending. If the first call fell back to a random port because the requested one was busy, later calls must use that returned port, not the originally requested value.

```python
import asyncio
import aio_sockets as aio
import aio_network_forward as aionf


async def main():
    local_port = await aionf.start_forward_tcp(("127.0.0.1", 9001), 8080)
    await aionf.start_forward_tcp(("127.0.0.1", 9002), local_port)
    await aionf.start_forward_tcp(("127.0.0.1", 9003), local_port)
    print(f"port {local_port} forwards to three backends")
    await aio.wait_all_tasks_done()


asyncio.run(main())
```

To remove one backend without stopping the whole listener, call `stop_forward_tcp(forward_to_addr)` or `stop_forward_udp(forward_to_addr)`. The local port stays open while other backends remain; the listener is closed only when the last backend is removed.

### Limit Listening Address to Localhost

By default, the module listens on `0.0.0.0` for IPv4 and `::` for IPv6. You can change that behavior before starting the forward.

```python
import asyncio
import aio_sockets as aio
import aio_network_forward as aionf


async def main():
    aionf.aio_network_forward._listen_ipv4 = "127.0.0.1"
    aionf.aio_network_forward._listen_ipv6 = "::1"

    forward_to = ("127.0.0.1", 8686)
    local_port = await aionf.start_forward_tcp(forward_to, 8585)
    print(f"listening on localhost:{local_port}")
    await aio.wait_all_tasks_done()


asyncio.run(main())
```

### HTTP Forward Demo

This repository includes `network_forward_demo.py`, which starts a local HTTP server on `127.0.0.1:8686`, then exposes it through a forwarding port.

```shell
python network_forward_demo.py
```

After startup, you can open the forwarded address shown in the logs, for example:

```text
http://127.0.0.1:8585
```

## API

### `start_forward_tcp(forward_to_addr, local_port) -> int`

Start a local TCP forward to `forward_to_addr` and return the actual listening port. If `local_port` is already forwarded by this module, append the target to the backend list instead of creating a new listener.

### `stop_forward_tcp(forward_to_addr) -> None`

Stop the TCP forward associated with `forward_to_addr`. If other backends share the same local port, only that backend is removed and related forwarded sockets are closed.

### `start_forward_udp(forward_to_addr, local_port) -> int`

Start a local UDP forward to `forward_to_addr` and return the actual listening port. If `local_port` is already forwarded by this module, append the target to the backend list instead of creating a new listener.

### `stop_forward_udp(forward_to_addr) -> None`

Stop the UDP forward associated with `forward_to_addr`. If other backends share the same local port, only that backend is removed.

## Module Configuration

These module-level variables can be adjusted before starting forwards:

| Variable | Default | Description |
| --- | --- | --- |
| `_listen_ipv4` | `"0.0.0.0"` | IPv4 bind address for local listeners |
| `_listen_ipv6` | `"::"` | IPv6 bind address for local listeners |
| `_tcp_connect_timeout` | `10` | Seconds to wait when connecting to a TCP backend; on timeout, the next backend is tried |
| `_tcp_read_size` | `8192` | TCP relay buffer size in bytes |
| `logger` | `StdoutLogger()` | Logger used for forwarding events |

Example:

```python
import aio_network_forward as aionf

aionf.aio_network_forward._tcp_connect_timeout = 5
aionf.aio_network_forward.logger = my_logger
```

## Notes

This project depends on `aio-sockets` for its socket wrapper utilities.

`start_forward_tcp()` and `start_forward_udp()` create background tasks only. In a standalone script, you should keep the main coroutine alive with `await aio.wait_all_tasks_done()`, otherwise the program will exit immediately after startup.

UDP forwarding keeps a temporary per-client forwarding socket and closes it automatically after a period of inactivity.
