Aethernet (Æthernet)
Full HTTP, WebSocket, and remote-access traffic over any message-based transport — strings or bytes.
Aethernet is a lightweight tunneling library that lets you keep using familiar httpx and WebSocket-style APIs even when you cannot open direct TCP connections. All traffic is framed, optionally encrypted, batched, and delivered through a LowTransport that you implement (Telegram, SMS, serial, BLE, sockets, etc.).
Why Aethernet?
Some environments can send/receive messages but cannot reach the internet directly: Telegram bots, SMS gateways, air-gapped systems, embedded radios, BLE links, and so on. Aethernet bridges that gap: your code keeps using standard HTTP/WebSocket clients while the real bytes travel over whatever channel you have.
Version 3.0.0 adds image-capable transports and Stasis — a full remote-access protocol (screen streaming + input forwarding) built on top of the same transport stack.
Features
- Drop-in HTTP client transport — compatible with
httpx.AsyncClient(transport=...) - WebSocket support — via
AethernetWebSockets - Generic, type-safe
LowTransport[T]with metaclass validationLowTransport[str]/LowTransport[bytes]for text or binary channelsLowTransport[StrAndImages]/LowTransport[BytesAndImages]for image-capable channels (required for Stasis)
- String or bytes low-level transport
mode="bytes"(default) for binary-safe channelsmode="string"for messengers/SMS (alphabet + size constraints)
- End-to-end encryption (optional) with selectable AEAD:
CHACHA20_POLY1305AES_GCMAES_EAX
- Reliability (Retry / ARQ) layer (optional)
NONE— best-effortSTOP_AND_WAIT— classic ARQPARALLEL— selective-repeat style with a configurable window
- Separate reliability mode for image frames — tune ARQ independently for screen frames vs. regular traffic
- Aggregation & flow control — batching and chunking over small-message channels
- Stasis — remote-access protocol: screen streaming and input forwarding; server runs inside
aethernet-core, client lives at github.com/solment-slet/stasis-client - Built-in TCP transport —
TCPLowTransportandTCPLowTransportServer, ready to use for testing or local setups athnetCLI — start the Aethernet server from the command line with a single command and a link config file- Bring your own transport — implement one abstract class (
LowTransport) and configure it viaLowTransportConfig
Installation
# pip
pip install aethernet-core
# uv
uv add aethernet-core
Public API
Everything below is available from the package root:
from aethernet import (
EncryptionMode,
ReliabilityMode,
LowTransport,
LowTransportConfig,
get_link,
AethernetHttpx,
AethernetWebSockets,
AethernetServer,
)
The built-in TCP transport lives under its own sub-package:
from aethernet.low_transports.aethernet_tcp_transport import (
TCPLowTransport,
TCPLowTransportServer,
)
Core concept: LowTransport[T]
LowTransport is the only class you implement. It is now generic: you declare what data type your channel carries, and the metaclass validates your declaration at class definition time — not at runtime.
LowTransport[str] # text-only channel
LowTransport[bytes] # binary channel
LowTransport[StrAndImages] # text channel that can also carry screen frames (Stasis)
LowTransport[BytesAndImages] # binary channel that can also carry screen frames (Stasis)
The metaclass enforces two rules automatically:
- You cannot pass the raw
TransportDataTypeVar — you must specify a concrete type. - If
supports_images=Truein yourCONFIG, you must useStrAndImagesorBytesAndImagesas the generic type.
A LowTransport must be able to:
send(data: T) -> Nonerecv() -> T
Configuration via LowTransportConfig
Transport limits and timings live in a dataclass and are typically provided via a class attribute CONFIG.
@dataclass(slots=True)
class LowTransportConfig:
mode: Literal["string", "bytes"] = "bytes"
supports_images: bool = False # set True to enable Stasis
max_message_bytes: int = 1024 # for bytes mode
max_message_chars: int | None = None # for string mode
alphabet: str | None = None # allowed chars in string mode
min_send_interval: float = 0.2
min_recv_interval: float = 0.2
delay_before_resending: float = 8.0 # used by the reliability layer
Every LowTransport subclass must:
- Accept
config: LowTransportConfig | None = Nonein__init__. - Call
super().__init__(config)inside__init__.
This lets users safely tweak individual fields with dataclasses.replace(...) and pass the modified config in.
Quick Start
Aethernet has two sides:
- Client — makes HTTP/WebSocket requests
- Server — runs on a machine with internet access and executes requests on behalf of the client
Both sides must use the same LowTransport implementation (or compatible ones), the same encryption_mode, the same encryption_key (if encryption is enabled), and the same reliability_mode.
1) Define your transport
from aethernet import LowTransport, LowTransportConfig
class MyTransport(LowTransport[bytes]):
CONFIG = LowTransportConfig(
mode="bytes",
max_message_bytes=1024,
min_send_interval=0.2,
min_recv_interval=0.2,
delay_before_resending=8.0,
)
def __init__(self, *, config: LowTransportConfig | None = None) -> None:
super().__init__(config)
# initialize your channel here (serial / socket / BLE / etc.)
def close(self) -> None:
pass # release resources
def send(self, data: bytes) -> None:
raise NotImplementedError
def recv(self) -> bytes:
raise NotImplementedError
2) Client side
import asyncio
import secrets
import httpx
from aethernet import get_link, AethernetWebSockets, EncryptionMode, ReliabilityMode
SHARED_KEY = secrets.token_bytes(32) # generate once, share securely
async def main() -> None:
link = await get_link(
MyTransport(),
encryption_mode=EncryptionMode.CHACHA20_POLY1305,
encryption_key=SHARED_KEY,
reliability_mode=ReliabilityMode.PARALLEL,
window_size=8,
)
# --- HTTP ---
async with httpx.AsyncClient(transport=link) as client:
r = await client.get("https://httpbin.org/get")
print(r.status_code, r.json())
# --- WebSocket ---
async with AethernetWebSockets(link).connect("wss://echo.websocket.events") as ws:
await ws.send("hello")
msg = await ws.recv()
print("Echo:", msg)
if __name__ == "__main__":
asyncio.run(main())
3) Server side
Run this where internet is available and where your LowTransport can exchange messages with the client.
import asyncio
from aethernet import AethernetServer, EncryptionMode, ReliabilityMode
SHARED_KEY = b"..." # must match the client's key exactly
async def main() -> None:
server = await AethernetServer.create(
MyTransport(),
encryption_mode=EncryptionMode.CHACHA20_POLY1305,
encryption_key=SHARED_KEY,
reliability_mode=ReliabilityMode.PARALLEL,
window_size=8,
)
print("Aethernet server is running...")
await server.start_and_wait()
if __name__ == "__main__":
asyncio.run(main())
String-mode transports (Telegram / SMS / etc.)
If your channel only supports text, use mode="string" and specify an alphabet and character limit.
from aethernet import LowTransport, LowTransportConfig
B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
class SmsLikeTransport(LowTransport[str]):
CONFIG = LowTransportConfig(
mode="string",
max_message_chars=1000,
alphabet=B64_ALPHABET,
min_send_interval=1.0,
min_recv_interval=1.0,
)
def __init__(self, *, config: LowTransportConfig | None = None) -> None:
super().__init__(config)
def send(self, data: str) -> None:
raise NotImplementedError
def recv(self) -> str:
raise NotImplementedError
Tweaking transport limits
Because config lives in a dataclass, you can override individual fields without subclassing:
from dataclasses import replace
cfg = replace(MyTransport.CONFIG, min_send_interval=0.6, max_message_bytes=2048)
low = MyTransport(config=cfg)
Built-in TCP transport
Aethernet ships with a ready-to-use TCP transport. It supports both regular bytes messages and image frames (BytesAndImages) for use with Stasis.
TCPLowTransport — single connection
Use this when you manage connection setup yourself: the client connects, and the server wraps an already-accepted socket.
from aethernet.low_transports.aethernet_tcp_transport import TCPLowTransport
# Client
transport = TCPLowTransport.connect("127.0.0.1", 9876)
# Server (manual accept loop)
server_sock = TCPLowTransport.listen("0.0.0.0", 9876)
transport = TCPLowTransport.accept(server_sock)
TCPLowTransport wraps a single socket. When that connection drops, the transport is done — it does not reconnect.
TCPLowTransportServer — self-managing server
Use this when you want the transport to survive client reconnections on its own. It listens on the given port internally and, whenever the active client disconnects, blocks until the next one connects — without tearing down the AggregatingLink above it.
from aethernet.low_transports.aethernet_tcp_transport import TCPLowTransportServer
transport = TCPLowTransportServer("0.0.0.0", 9876, logger=logger)
link = await get_link(transport, ...)
# clients can disconnect and reconnect freely — the link stays alive
Both classes enable TCP keepalive by default (configurable via keepalive_idle, keepalive_interval, keepalive_count), which ensures that a peer that disappears without a clean disconnect is detected within roughly 10–15 seconds rather than hanging forever.
athnet CLI
Version 3.0.0 ships a console command, athnet, that starts the Aethernet server without writing any boilerplate. You describe your transport once in a link config file (aethernet_link.py), and athnet takes care of the rest — including automatic restart if the link breaks fatally.
Link config file
The link config file must expose a single async function:
async def create_link(
mode: Literal["server", "client"], logger: logging.Logger
) -> AggregatingLink:
...
mode is "server" when called from athnet and "client" when called from client-side tooling such as stasis-client. This lets you describe both sides of the connection in one file.
A minimal example using the built-in TCP transport:
# aethernet_link.py
import asyncio
from aethernet import ReliabilityMode, get_link
from aethernet.low_transports.aethernet_tcp_transport import (
TCPLowTransport,
TCPLowTransportServer,
)
async def create_link(mode, logger):
if mode == "client":
low_transport = await asyncio.to_thread(
TCPLowTransport.connect, "127.0.0.1", 9876
)
else:
low_transport = await asyncio.to_thread(
TCPLowTransportServer, "127.0.0.1", 9876
)
return await get_link(
low_transport,
reliability_mode=ReliabilityMode.STOP_AND_WAIT,
image_reliability_mode=ReliabilityMode.NONE,
logger=logger,
)
A fully documented template (aethernet_link.example.py) is included in the package source.
Running the server
# uses ./aethernet_link.py by default
athnet
# explicit config path
athnet --link-config /path/to/my_link.py
# enable specific protocols only
athnet --protocols http,websocket
# tune logging
athnet --log-level DEBUG --log-file /var/log/aethernet.log
CLI reference
| Flag | Default | Env var | Description |
|---|---|---|---|
--link-config |
./aethernet_link.py |
AETHERNET_LINK_CONFIG |
Path to the link config file |
--log-level |
DEBUG |
AETHERNET_LOG_LEVEL |
DEBUG / INFO / WARNING / ERROR / CRITICAL |
--protocols |
all | AETHERNET_PROTOCOLS |
Comma-separated list of protocols to enable |
--log-file |
stderr only | AETHERNET_LOG_FILE |
Path to a rotating log file |
--log-max-bytes |
10 MiB | AETHERNET_LOG_MAX_BYTES |
Log rotation size |
--log-backup-count |
5 | AETHERNET_LOG_BACKUP_COUNT |
Number of rotated log files to keep |
All flags can be set via environment variables instead, which is convenient for systemd units or Docker environments.
Stasis — remote access protocol
Stasis is a remote-access protocol built on top of the Aethernet transport stack. It streams the remote device's screen to the client and forwards keyboard, mouse, and clipboard events back. The server (the remote device being controlled) runs inside aethernet-core; the client lives in a separate repository:
To use Stasis, your LowTransport must be able to carry image frames — declare it as LowTransport[BytesAndImages] (or LowTransport[StrAndImages]) and set supports_images=True in its CONFIG. The metaclass will reject any other combination at class-definition time.
The built-in TCPLowTransport and TCPLowTransportServer already satisfy this requirement out of the box.
Transport stack: get_link
get_link assembles the full three-layer stack (low → medium → aggregating) and returns an AggregatingLink ready for use with httpx, WebSockets, or Stasis.
async def get_link(
low_transport: LowTransport,
*,
# Encryption
encryption_mode: EncryptionMode = EncryptionMode.NONE,
encryption_key: bytes | None = None,
# Aggregating
flush_interval: float = 0.5,
max_batch_size: int = 64 * 1024,
chunk_assembly_ttl: float = 60.0,
# Reliability
reliability_mode: ReliabilityMode = ReliabilityMode.NONE,
image_reliability_mode: ReliabilityMode = ReliabilityMode.NONE,
window_size: int = 8,
ack_flush_interval: float = 0.1,
ack_batch_size: int = 8,
received_seqs_window: int = 256,
reorder_buffer_ttl: float = 500.0,
# Logging
logger=None,
) -> AggregatingLink
Renamed in 3.0.0 —
get_transportfrom earlier versions is nowget_link.
Parameters
Encryption
encryption_mode—NONE(default),CHACHA20_POLY1305,AES_GCM, orAES_EAXencryption_key— shared secret (required whenencryption_mode != NONE)
Aggregating
flush_interval— max time to wait before flushing a batchmax_batch_size— max logical batch size in bytes before chunkingchunk_assembly_ttl— lifetime of an incomplete chunk assembly
Reliability
reliability_mode— ARQ mode for regular messages:NONE,STOP_AND_WAIT, orPARALLELimage_reliability_mode— ARQ mode for image frames (Stasis); tuned independently from regular trafficwindow_size— send window forPARALLELmodeack_flush_interval,ack_batch_size— ACK pacing and batchingreceived_seqs_window— deduplication memory (number of recent sequence numbers)reorder_buffer_ttl— how long to hold out-of-order packets while reordering
Logging
logger— optional standard libraryLoggerfor debugging and observability
Server creation: AethernetServer.create
AethernetServer.create accepts the same parameters as get_link and additionally configures the HTTP router and SSE flushing:
@classmethod
async def create(
cls,
low_transport: LowTransport,
*,
# same as get_link ...
# Server router
http_client=...,
proxy_http_client=...,
# SSE streaming
sse_flush_bytes: int = 65536,
sse_flush_interval: float = 0.5,
logger=...,
) -> AethernetServer
Extra parameters:
http_client— thehttpx.AsyncClientused for outgoing requestsproxy_http_client— optional separate client for proxied requestssse_flush_bytes,sse_flush_interval— flushing behaviour for SSE streaming responses
Enums
from aethernet import ReliabilityMode, EncryptionMode
ReliabilityMode.NONE
ReliabilityMode.STOP_AND_WAIT
ReliabilityMode.PARALLEL
EncryptionMode.NONE
EncryptionMode.CHACHA20_POLY1305
EncryptionMode.AES_GCM
EncryptionMode.AES_EAX
Project structure
aethernet-core/
├── .github/
├── src/
│ └── aethernet/
│ ├── low_transports/
│ │ └── aethernet_tcp_transport/
│ │ ├── low_transport.py # TCPLowTransport, TCPLowTransportServer
│ │ └── aethernet_link.py # ready-to-use link file for athnet
│ ├── transport/
│ ├── __init__.py
│ ├── cli.py
│ ├── exceptions.py
│ ├── server_router.py
│ └── typing.py
├── tests/
├── aethernet_link.example.py
├── pyproject.toml
├── .gitignore
├── LICENSE
├── NOTICE
├── uv.lock
├── README.ru.md
└── README.md
Migration from 2.x
| 2.x | 3.0.0 |
|---|---|
get_transport(...) |
get_link(...) |
LowTransport (no generic) |
LowTransport[str] / LowTransport[bytes] / LowTransport[StrAndImages] / LowTransport[BytesAndImages] |
| No image support | supports_images=True + BytesAndImages / StrAndImages |
| No built-in TCP transport | TCPLowTransport, TCPLowTransportServer |
| No CLI | athnet command + aethernet_link.py |
| No Stasis | Remote-access protocol, client at solment-slet/stasis-client |
Development
uv sync --extra dev
pytest
ruff check .
mypy .
Compatibility
| Python | Platforms |
|---|---|
| 3.12 + | Linux, macOS, Windows |
Bug Reports
Please open an issue and include:
- Expected behaviour
- Actual behaviour
- Minimal reproduction
License
Author
Walter Kerrigan (esolment) — https://github.com/esolment
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aethernet_core-3.0.0.tar.gz.
File metadata
- Download URL: aethernet_core-3.0.0.tar.gz
- Upload date:
- Size: 83.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
266d36f111bca071ef5c0d059892a0ecc18592cfe493e714f278573d68076fd6
|
|
| MD5 |
adaf4c1d8ef56059e723207b35ef34c9
|
|
| BLAKE2b-256 |
b6711e96b56eab184482f3ef783c202efa7700b4ee50d0f3106304f15736f1f8
|
Provenance
The following attestation bundles were made for aethernet_core-3.0.0.tar.gz:
Publisher:
release.yml on solment-slet/aethernet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aethernet_core-3.0.0.tar.gz -
Subject digest:
266d36f111bca071ef5c0d059892a0ecc18592cfe493e714f278573d68076fd6 - Sigstore transparency entry: 2439111894
- Sigstore integration time:
-
Permalink:
solment-slet/aethernet@d63e709c55a27e400f9598319f5b179cbc790c7b -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/solment-slet
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d63e709c55a27e400f9598319f5b179cbc790c7b -
Trigger Event:
release
-
Statement type:
File details
Details for the file aethernet_core-3.0.0-py3-none-any.whl.
File metadata
- Download URL: aethernet_core-3.0.0-py3-none-any.whl
- Upload date:
- Size: 83.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4746495a19945a8742603efd1748792d41d75e1aa26c6f72feb7fbbbbace743
|
|
| MD5 |
a9041ba55fc50db7dd20879c1cf4003c
|
|
| BLAKE2b-256 |
b5c5624ee82d25cdd5e92ceb549aa713733f4fc87ac465f618491f43167772ff
|
Provenance
The following attestation bundles were made for aethernet_core-3.0.0-py3-none-any.whl:
Publisher:
release.yml on solment-slet/aethernet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aethernet_core-3.0.0-py3-none-any.whl -
Subject digest:
b4746495a19945a8742603efd1748792d41d75e1aa26c6f72feb7fbbbbace743 - Sigstore transparency entry: 2439111926
- Sigstore integration time:
-
Permalink:
solment-slet/aethernet@d63e709c55a27e400f9598319f5b179cbc790c7b -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/solment-slet
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d63e709c55a27e400f9598319f5b179cbc790c7b -
Trigger Event:
release
-
Statement type: