Python relay/proxy server and library to build reliable encrypted TCP/UDP tunnels with entropy control for TCP traffic


Keywords
asyncio, http, https, tcp, udp, proxy, tunnel, relay, network, encrypted, cipher, security, censorship, entropy
License
MIT
Install
pip install ouija==1.3.1

Documentation

Ouija

Python relay/proxy server and library to build reliable encrypted TCP/UDP tunnels with entropy control for TCP traffic

pypi version

Features

  • Easy to install, configure and use
  • TCP/UDP tunneling
  • Pluggable traffic ciphers
  • Pluggable traffic entropy control

Hides TCP traffic in encrypted TCP/UDP tunnel between relay and proxy servers

TCP/UDP tunneling

Requirements

  • Python 3.11
  • pbjson 1.18.0
  • cryptography 41.0.2
  • numpy 1.25.2

Install

python3.11 -m venv .env
source .env/bin/activate
pip install ouija

Usage

Generate cipher_key/token secrets:

ouija_secret

Run relay/proxy server:

ouija <config.json>

tcp-relay.json - TCP relay server - HTTP/HTTPS proxy server interface with TCP connectors:

{
  "protocol": "TCP",
  "mode": "RELAY",
  "debug": true,
  "monitor": true,
  "relay_host": "127.0.0.1",
  "relay_port": 9000,
  "proxy_host": "127.0.0.1",
  "proxy_port": 50000,
  "cipher_key": "bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI=",
  "entropy_rate": 5,
  "token": "395f249c-343a-4f92-9129-68c6d83b5f55",
  "serving_timeout": 20.0,
  "tcp_buffer": 1024,
  "tcp_timeout": 1.0,
  "message_timeout": 5.0
}

tcp-proxy.json - TCP-relayed proxy server:

{
  "protocol": "TCP",
  "mode": "PROXY",
  "debug": true,
  "monitor": true,
  "proxy_host": "0.0.0.0",
  "proxy_port": 50000,
  "cipher_key": "bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI=",
  "entropy_rate": 5,
  "token": "395f249c-343a-4f92-9129-68c6d83b5f55",
  "serving_timeout": 20.0,
  "tcp_buffer": 1024,
  "tcp_timeout": 1.0,
  "message_timeout": 5.0
}

udp-relay.json - UDP relay server - HTTP/HTTPS proxy server interface with UDP connectors:

{
  "protocol": "UDP",
  "mode": "RELAY",
  "debug": true,
  "monitor": true,
  "relay_host": "127.0.0.1",
  "relay_port": 9000,
  "proxy_host": "127.0.0.1",
  "proxy_port": 50000,
  "cipher_key": "bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI=",
  "entropy_rate": 5,
  "token": "395f249c-343a-4f92-9129-68c6d83b5f55",
  "serving_timeout": 20.0,
  "tcp_buffer": 1024,
  "tcp_timeout": 1.0,
  "udp_min_payload": 512,
  "udp_max_payload": 1024,
  "udp_timeout": 2.0,
  "udp_retries": 5,
  "udp_capacity": 10000,
  "udp_resend_sleep": 0.25
}

udp-proxy.json - UDP-relayed proxy server:

{
  "protocol": "UDP",
  "mode": "PROXY",
  "debug": true,
  "monitor": true,
  "proxy_host": "0.0.0.0",
  "proxy_port": 50000,
  "cipher_key": "bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI=",
  "entropy_rate": 5,
  "token": "395f249c-343a-4f92-9129-68c6d83b5f55",
  "serving_timeout": 20.0,
  "tcp_buffer": 1024,
  "tcp_timeout": 1.0,
  "udp_min_payload": 512,
  "udp_max_payload": 1024,
  "udp_timeout": 2.0,
  "udp_retries": 5,
  "udp_capacity": 10000,
  "udp_resend_sleep": 0.25
}

Relay and proxy setup configuration with supervisord - ouija-config

Cipher and entropy

  • cipher_key - FernetCipher key - use ouija_secret to generate key
  • entropy_rate - SimpleEntropy rate, when rate=N every Nth byte will be generated and payload size will increase, rate=5 means 20% traffic overhead

Protocols

  • Stream - TCP
  • Datagram - UDP

Entities

  • Cipher - cipher implementation - FernetCipher out of the box
  • Entropy - entropy control implementation - SimpleEntropy out of the box
  • Tuning - relay/proxy and connector/link interaction settings
  • Relay - HTTPS proxy server interface
  • Connector - relay connector, which communicates with proxy link
  • Proxy - proxy server, which gets requests from relay and sends back responses from remote servers
  • Link - proxy link with relay connector

Tuning - TCP

  • cipher - cipher instance, if None then no encryption will be applied
  • entropy - entropy instance, if None then no entropy control will be applied
  • token - your secret token - UUID4 or anything else - use ouija_secret to generate token
  • serving_timeout - timeout for serve/resend workers, 2X for handlers, seconds
  • tcp_buffer - TCP buffer size, bytes
  • tcp_timeout - TCP awaiting timeout, seconds
  • message_timeout - TCP service message timeout, seconds

Tuning - UDP

  • cipher - cipher instance, if None then no encryption will be applied
  • entropy - entropy instance, if None then no entropy control will be applied
  • token - your secret token - UUID4 or anything else - use ouija_secret to generate token
  • serving_timeout - timeout for serve/resend workers, 2X for handlers, seconds
  • tcp_buffer - TCP buffer size, bytes
  • tcp_timeout - TCP awaiting timeout, seconds
  • udp_min_payload - UDP min payload size, bytes
  • udp_max_payload - UDP max payload size, bytes
  • udp_timeout - UDP awaiting timeout, seconds
  • udp_retries - UDP max retry count per interaction
  • udp_capacity - UDP send/receive buffer capacity - max packet count
  • udp_resend_sleep - UDP resend sleep between retries, seconds

Library usage

stream-relay.py - TCP relay server - HTTP/HTTPS proxy server interface with TCP connectors:

import asyncio
import logging

from ouija import StreamRelay as Relay, StreamTuning as Tuning, Telemetry, SimpleEntropy, FernetCipher


async def main() -> None:
    tuning = Tuning(
        cipher=FernetCipher(key='bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI='),
        entropy=SimpleEntropy(rate=5),
        token='395f249c-343a-4f92-9129-68c6d83b5f55',
        serving_timeout=20.0,
        tcp_buffer=1024,
        tcp_timeout=1.0,
        message_timeout=5.0,
    )
    relay = Relay(
        telemetry=Telemetry(),
        tuning=tuning,
        relay_host='127.0.0.1',
        relay_port=9000,
        proxy_host='127.0.0.1',
        proxy_port=50000,
    )
    asyncio.create_task(relay.debug())
    await relay.serve()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.run_forever()

stream-proxy.py - TCP-relayed proxy server:

import asyncio
import logging

from ouija import StreamProxy as Proxy, Telemetry, StreamTuning as Tuning, SimpleEntropy, FernetCipher


async def main() -> None:
    tuning = Tuning(
        cipher=FernetCipher(key='bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI='),
        entropy=SimpleEntropy(rate=5),
        token='395f249c-343a-4f92-9129-68c6d83b5f55',
        serving_timeout=20.0,
        tcp_buffer=1024,
        tcp_timeout=1.0,
        message_timeout=5.0,
    )
    proxy = Proxy(
        telemetry=Telemetry(),
        tuning=tuning,
        proxy_host='0.0.0.0',
        proxy_port=50000,
    )
    asyncio.create_task(proxy.debug())
    await proxy.serve()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.run_forever()

datagram-relay.py - UDP relay server - HTTPS proxy server interface with UDP connectors:

import asyncio
import logging

from ouija import DatagramRelay as Relay, DatagramTuning as Tuning, Telemetry, SimpleEntropy, FernetCipher


async def main() -> None:
    tuning = Tuning(
        cipher=FernetCipher(key='bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI='),
        entropy=SimpleEntropy(rate=5),
        token='395f249c-343a-4f92-9129-68c6d83b5f55',
        serving_timeout=20.0,
        tcp_buffer=1024,
        tcp_timeout=1.0,
        udp_min_payload=512,
        udp_max_payload=1024,
        udp_timeout=2.0,
        udp_retries=5,
        udp_capacity=10000,
        udp_resend_sleep=0.25,
    )
    relay = Relay(
        telemetry=Telemetry(),
        tuning=tuning,
        relay_host='127.0.0.1',
        relay_port=9000,
        proxy_host='127.0.0.1',
        proxy_port=50000,
    )
    asyncio.create_task(relay.debug())
    await relay.serve()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.run_forever()

datagram-proxy.py - UDP-relayed proxy server:

import asyncio
import logging

from ouija import DatagramProxy as Proxy, Telemetry, DatagramTuning as Tuning, SimpleEntropy, FernetCipher


async def main() -> None:
    tuning = Tuning(
        cipher=FernetCipher(key='bdDmN4VexpDvTrs6gw8xTzaFvIBobFg1Cx2McFB1RmI='),
        entropy=SimpleEntropy(rate=5),
        token='395f249c-343a-4f92-9129-68c6d83b5f55',
        serving_timeout=20.0,
        tcp_buffer=1024,
        tcp_timeout=1.0,
        udp_min_payload=512,
        udp_max_payload=1024,
        udp_timeout=2.0,
        udp_retries=5,
        udp_capacity=10000,
        udp_resend_sleep=0.25,
    )
    proxy = Proxy(
        telemetry=Telemetry(),
        tuning=tuning,
        proxy_host='0.0.0.0',
        proxy_port=50000,
    )
    asyncio.create_task(proxy.debug())
    await proxy.serve()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.run_forever()

Tests

pytest --cov-report html:htmlcov --cov=ouija tests/