Skip to main content

Full HTTP and WebSocket traffic over any text-based transport — messengers, SMS, or anything else that can carry a string.

Project description

Aethernet (Æthernet)

Full HTTP and WebSocket 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, etc. Aethernet bridges that gap: your code keeps using standard HTTP/WebSocket clients while the real bytes travel over whatever channel you have.


Features

  • Drop-in HTTP client transport — compatible with httpx.AsyncClient(transport=...)
  • WebSocket support — via AethernetWebSockets
  • String or bytes low-level transport
    • mode="bytes" (default) for binary-safe channels
    • mode="string" for messengers/SMS (alphabet + size constraints)
  • End-to-end encryption (optional) with selectable AEAD:
    • CHACHA20_POLY1305
    • AES_GCM
    • AES_EAX
  • Reliability (Retry / ARQ) layer (optional)
    • NONE — best-effort
    • STOP_AND_WAIT — classic ARQ
    • PARALLEL — selective-repeat style with a window
  • Aggregation & flow control — batching and chunking over small-message channels
  • Bring your own transport — implement one abstract class (LowTransport) and configure it via LowTransportConfig

Installation

# pip
pip install aethernet-core

# uv
uv add aethernet-core

Public API (what you import)

Everything below is available from the package root:

from aethernet import (
    EncryptionMode,
    ReliabilityMode,
    LowTransport,
    LowTransportConfig,
    get_transport,
    AethernetHttpx,
    AethernetWebSockets,
    AethernetServer,
)

Core concept: LowTransport

A LowTransport is the only thing you implement. It must be able to:

  • send(data: str | bytes) -> None
  • recv() -> str | bytes

Configuration via CONFIG (important)

Transport limits and timings live in a dataclass LowTransportConfig and are typically provided via a class attribute CONFIG.

Key points:

  1. You can keep the default CONFIG, but it’s recommended to set it explicitly for your transport.
  2. Every implementor must:
    • accept config: LowTransportConfig | None = None in __init__
    • call super().__init__(config) inside __init__

This enables users to safely tweak parameters with dataclasses.replace(...) and pass the modified config to your transport.


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)
  • the same reliability_mode

1) Define your transport (bytes-mode example)

from aethernet import LowTransport, LowTransportConfig


class MyTransport(LowTransport):
    # Recommended: define constraints/timings here
    CONFIG = LowTransportConfig(
        mode="bytes",
        max_message_bytes=1024,
        min_send_interval=0.2,
        min_recv_interval=0.2,
        delay_before_resending=8.0,  # used by reliability layer
    )

    def __init__(self, *, config: LowTransportConfig | None = None) -> None:
        super().__init__(config)
        # initialize your channel here (serial/socket/BLE/etc.)

    def close(self) -> None:
        # release resources (optional)
        pass

    def send(self, data:  bytes) -> None:
        # deliver `data` through your channel
        raise NotImplementedError

    def recv(self) -> bytes:
        # return the next incoming message
        raise NotImplementedError

2) Client side

import asyncio
import secrets
import httpx
from aethernet import (
    get_transport,
    AethernetWebSockets,
    EncryptionMode,
    ReliabilityMode,
)


SHARED_KEY = secrets.token_bytes(32)  # generate once, share securely


async def main() -> None:
    link = await get_transport(
        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 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 (allowed characters) and limits.

from aethernet import LowTransport, LowTransportConfig

B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="


class SmsLikeTransport(LowTransport):
    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:
        # In string mode you will typically send `str` messages
        # (the stack will produce appropriate payloads for your mode).
        raise NotImplementedError

    def recv(self) -> str:
        raise NotImplementedError

Tweaking transport limits with dataclasses.replace

Because config lives in a dataclass, users can override just a few fields:

from dataclasses import replace

cfg = replace(MyTransport.CONFIG, min_send_interval=0.6, max_message_bytes=2048)
low = MyTransport(config=cfg)

Transport stack creation: get_transport(...)

async def get_transport(
    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,
    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"

Parameters overview

Encryption

  • encryption_mode
    • EncryptionMode.NONE
    • EncryptionMode.CHACHA20_POLY1305
    • EncryptionMode.AES_GCM
    • EncryptionMode.AES_EAX
  • encryption_key: shared secret key (must match on both ends). Required if encryption_mode != NONE.

Aggregating (batching/chunking)

  • flush_interval: max time to wait before flushing a batch
  • max_batch_size: max logical batch size in bytes (before chunking)
  • chunk_assembly_ttl: time to keep incomplete chunk assemblies

Reliability (Retry / ARQ)

  • reliability_mode
    • ReliabilityMode.NONE
    • ReliabilityMode.STOP_AND_WAIT
    • ReliabilityMode.PARALLEL (uses window_size)
  • window_size: send window for PARALLEL
  • ack_flush_interval, ack_batch_size: ACK pacing / batching
  • received_seqs_window: deduplication memory
  • reorder_buffer_ttl: how long to keep out-of-order packets while reordering

Logging

  • logger: optional logger for debugging/observability

Server creation: AethernetServer.create(...)

AethernetServer.create(...) mirrors get_transport(...) and additionally configures the HTTP router and SSE flushing:

@classmethod
async def create(
    cls,
    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,
    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,
    # Server Router
    http_client=...,
    proxy_http_client=...,
    # HTTP Aggregating
    sse_flush_bytes: int = 65536,
    sse_flush_interval: float = 0.5,
    # Logging
    logger=...,
) -> "AethernetServer"

Extra server parameters

  • http_client: the httpx.AsyncClient used by the server to perform real outgoing requests
  • proxy_http_client: optional separate client (e.g. for proxying)
  • sse_flush_bytes, sse_flush_interval: flushing behavior for SSE streaming responses

Enums

Reliability

from aethernet import ReliabilityMode

ReliabilityMode.NONE
ReliabilityMode.STOP_AND_WAIT
ReliabilityMode.PARALLEL

Encryption

from aethernet import EncryptionMode

EncryptionMode.NONE
EncryptionMode.CHACHA20_POLY1305
EncryptionMode.AES_GCM
EncryptionMode.AES_EAX

Project Structure

aethernet-core/
├── .github/
├── src/
│   └── aethernet/
│       ├── transport/
│       ├── __init__.py
│       ├── exceptions.py
│       ├── server_router.py
│       └── typing.py
├── tests/
├── pyproject.toml
├── .gitignore
├── LICENSE
├── NOTICE
├── uv.lock
├── README.ru.md
└── README.md

Development

uv sync --extra dev
pytest

black --check .
ruff check .
mypy .

Compatibility

Python Platforms
3.12 + Linux, macOS, Windows

Bug Reports

Please open an issue and include:

  1. Expected behavior
  2. Actual behavior
  3. Minimal reproduction

License

Apache License 2.0


Author

Walter Kerrigan (esolment)https://github.com/esolment

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aethernet_core-2.0.1.tar.gz (53.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aethernet_core-2.0.1-py3-none-any.whl (42.9 kB view details)

Uploaded Python 3

File details

Details for the file aethernet_core-2.0.1.tar.gz.

File metadata

  • Download URL: aethernet_core-2.0.1.tar.gz
  • Upload date:
  • Size: 53.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aethernet_core-2.0.1.tar.gz
Algorithm Hash digest
SHA256 18fc70dd397c4e35af4c56b89b0cbb6830545e366cc4f685c2e8ca85aed1b70f
MD5 f05e71da0e3c3f55cd96bc9279da478f
BLAKE2b-256 09ecfaf37130b50c1e003ea7bea74d03276eede4d55d2414505e5d03c02ab8bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for aethernet_core-2.0.1.tar.gz:

Publisher: release.yml on solment-slet/aethernet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aethernet_core-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: aethernet_core-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 42.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aethernet_core-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 aeccfbcce18a73806bd6716cba2091c6ffefdabaf0da705c4ea639d8e5505563
MD5 008b23a828086026785f8a23eef49520
BLAKE2b-256 dab15316c5496934fcfcbae63dc6668b287f5f120f458291345e993bc8b3cff2

See more details on using hashes here.

Provenance

The following attestation bundles were made for aethernet_core-2.0.1-py3-none-any.whl:

Publisher: release.yml on solment-slet/aethernet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page