add transparent server mode based on WireGuard (#5562)
* add mode spec for WireGuard mode * add WireGuard server implementation * remove coverage excludes * simplify wireguard spec * lint! * remove superfluous tests * bump to mitmproxy_wireguard 0.1.1 * proxy/test_mode_specs: remove unused import * fix wireguard server mode * WireGuard: move keyfile gen into `.start()` This way any file format errors result in `.last_exception` being set. * fixup UDP support * bump to mitmproxy_wireguard v0.1.2 This release fixes TCP connections which were broken in v0.1.1. * fix crash handler * add simple test for WireGuard server instances * bump to mitmproxy_wireguard v0.1.5 and fix launching wg-test-client * fixups - monkeypatch `handle_client` instead of the handlers. - fix OS detection - ctx.log -> logging * nits * bump to mitmproxy_wireguard 0.1.6 for fixed test client * move WireGuardDatagramTransport into dedicated module this allows us to exclude it from individual coverage, which makes no sense. Also improve type checking to make sure that it's a full replacement. * cover WireGuardServerInstance.is_running property with tests * enable specialized server instance creation * test wireguard conf generation * deduplicate tcp/udp handlers * update CHANGELOG Co-authored-by: Maximilian Hils <git@maximilianhils.com>
This commit is contained in:
parent
12e2aecdf9
commit
2d495c093c
|
@ -37,6 +37,8 @@
|
|||
([#5590](https://github.com/mitmproxy/mitmproxy/pull/5590), @mhils)
|
||||
* Add MQTT content view.
|
||||
([#5588](https://github.com/mitmproxy/mitmproxy/pull/5588), @nikitastupin, @abbbe)
|
||||
* Add WireGuard mode to enable userspace transparent proxying via WireGuard.
|
||||
([#5562](https://github.com/mitmproxy/mitmproxy/pull/5562), @decathorpe, @mhils)
|
||||
|
||||
## 28 June 2022: mitmproxy 8.1.1
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ import asyncio
|
|||
import logging
|
||||
import socket
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
|
||||
from mitmproxy.connection import Address
|
||||
from mitmproxy.net import udp_wireguard
|
||||
from mitmproxy.utils import human
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -27,7 +29,6 @@ SockAddress = Union[tuple[str, int], tuple[str, int, int, int]]
|
|||
|
||||
|
||||
class DrainableDatagramProtocol(asyncio.DatagramProtocol):
|
||||
|
||||
_loop: asyncio.AbstractEventLoop
|
||||
_closed: asyncio.Event
|
||||
_paused: int
|
||||
|
@ -98,6 +99,7 @@ class UdpServer(DrainableDatagramProtocol):
|
|||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
if self._transport is None:
|
||||
self._transport = cast(asyncio.DatagramTransport, transport)
|
||||
self._transport.set_protocol(self)
|
||||
self._local_addr = transport.get_extra_info("sockname")
|
||||
super().connection_made(transport)
|
||||
|
||||
|
@ -112,7 +114,6 @@ class UdpServer(DrainableDatagramProtocol):
|
|||
|
||||
|
||||
class DatagramReader:
|
||||
|
||||
_packets: asyncio.Queue
|
||||
_eof: bool
|
||||
|
||||
|
@ -157,7 +158,6 @@ class DatagramReader:
|
|||
|
||||
|
||||
class DatagramWriter:
|
||||
|
||||
_transport: asyncio.DatagramTransport
|
||||
_remote_addr: Address
|
||||
_reader: DatagramReader | None
|
||||
|
@ -175,14 +175,12 @@ class DatagramWriter:
|
|||
"""
|
||||
self._transport = transport
|
||||
self._remote_addr = remote_addr
|
||||
proto = transport.get_protocol()
|
||||
assert isinstance(proto, DrainableDatagramProtocol)
|
||||
self._reader = reader
|
||||
self._closed = asyncio.Event() if reader is not None else None
|
||||
|
||||
@property
|
||||
def _protocol(self) -> DrainableDatagramProtocol:
|
||||
return cast(DrainableDatagramProtocol, self._transport.get_protocol())
|
||||
def _protocol(self) -> DrainableDatagramProtocol | udp_wireguard.WireGuardDatagramTransport:
|
||||
return self._transport.get_protocol() # type: ignore
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self._transport.sendto(data, self._remote_addr)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
This module contains a mock DatagramTransport for use with mitmproxy-wireguard.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import mitmproxy_wireguard as wg
|
||||
|
||||
from mitmproxy.connection import Address
|
||||
|
||||
|
||||
class WireGuardDatagramTransport(asyncio.DatagramTransport):
|
||||
def __init__(self, server: wg.Server, local_addr: Address, remote_addr: Address):
|
||||
self._server: wg.Server = server
|
||||
self._local_addr: Address = local_addr
|
||||
self._remote_addr: Address = remote_addr
|
||||
super().__init__()
|
||||
|
||||
def sendto(self, data, addr=None):
|
||||
self._server.send_datagram(data, self._local_addr, addr or self._remote_addr)
|
||||
|
||||
def get_extra_info(self, name: str, default: Any = None) -> Any:
|
||||
if name == "sockname":
|
||||
return self._server.getsockname()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_protocol(self):
|
||||
return self
|
||||
|
||||
async def drain(self) -> None:
|
||||
pass
|
||||
|
||||
async def wait_closed(self) -> None:
|
||||
pass
|
|
@ -12,25 +12,29 @@ Example:
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import errno
|
||||
import socket
|
||||
import textwrap
|
||||
import typing
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Generic, TypeVar, cast, get_args
|
||||
|
||||
import errno
|
||||
import mitmproxy_wireguard as wg
|
||||
|
||||
from mitmproxy import ctx, flow, platform
|
||||
from mitmproxy.connection import Address
|
||||
from mitmproxy.master import Master
|
||||
from mitmproxy.net import udp
|
||||
from mitmproxy.net import local_ip, udp
|
||||
from mitmproxy.net.udp_wireguard import WireGuardDatagramTransport
|
||||
from mitmproxy.proxy import commands, layers, mode_specs, server
|
||||
from mitmproxy.proxy.context import Context
|
||||
from mitmproxy.proxy.layer import Layer
|
||||
from mitmproxy.utils import human
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -62,8 +66,11 @@ class ServerManager(typing.Protocol):
|
|||
... # pragma: no cover
|
||||
|
||||
|
||||
class ServerInstance(Generic[M], metaclass=ABCMeta):
|
||||
# Python 3.11: Use typing.Self
|
||||
Self = TypeVar("Self", bound="ServerInstance")
|
||||
|
||||
|
||||
class ServerInstance(Generic[M], metaclass=ABCMeta):
|
||||
__modes: ClassVar[dict[str, type[ServerInstance]]] = {}
|
||||
|
||||
def __init__(self, mode: M, manager: ServerManager):
|
||||
|
@ -80,14 +87,20 @@ class ServerInstance(Generic[M], metaclass=ABCMeta):
|
|||
assert mode.type not in ServerInstance.__modes
|
||||
ServerInstance.__modes[mode.type] = cls
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
def make(
|
||||
cls: typing.Type[Self],
|
||||
mode: mode_specs.ProxyMode | str,
|
||||
manager: ServerManager,
|
||||
) -> ServerInstance:
|
||||
) -> Self:
|
||||
if isinstance(mode, str):
|
||||
mode = mode_specs.ProxyMode.parse(mode)
|
||||
return ServerInstance.__modes[mode.type](mode, manager)
|
||||
inst = ServerInstance.__modes[mode.type](mode, manager)
|
||||
|
||||
if not isinstance(inst, cls):
|
||||
raise ValueError(f"{mode!r} is not a spec for a {cls.__name__} server.")
|
||||
|
||||
return inst
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
@ -107,6 +120,10 @@ class ServerInstance(Generic[M], metaclass=ABCMeta):
|
|||
def listen_addrs(self) -> tuple[Address, ...]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def make_top_layer(self, context: Context) -> Layer:
|
||||
pass
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {
|
||||
"type": self.mode.type,
|
||||
|
@ -117,15 +134,67 @@ class ServerInstance(Generic[M], metaclass=ABCMeta):
|
|||
"listen_addrs": self.listen_addrs,
|
||||
}
|
||||
|
||||
async def handle_tcp_connection(
|
||||
self,
|
||||
reader: asyncio.StreamReader | wg.TcpStream,
|
||||
writer: asyncio.StreamWriter | wg.TcpStream,
|
||||
) -> None:
|
||||
connection_id = (
|
||||
"tcp",
|
||||
writer.get_extra_info("peername"),
|
||||
writer.get_extra_info("sockname"),
|
||||
)
|
||||
handler = ProxyConnectionHandler(
|
||||
ctx.master, reader, writer, ctx.options, self.mode
|
||||
)
|
||||
handler.layer = self.make_top_layer(handler.layer.context)
|
||||
if isinstance(self.mode, mode_specs.TransparentMode):
|
||||
socket = writer.get_extra_info("socket")
|
||||
try:
|
||||
assert platform.original_addr
|
||||
handler.layer.context.server.address = platform.original_addr(socket)
|
||||
except Exception as e:
|
||||
logger.error(f"Transparent mode failure: {e!r}")
|
||||
return
|
||||
with self.manager.register_connection(connection_id, handler):
|
||||
await handler.handle_client()
|
||||
|
||||
def handle_udp_datagram(
|
||||
self,
|
||||
transport: asyncio.DatagramTransport,
|
||||
data: bytes,
|
||||
remote_addr: Address,
|
||||
local_addr: Address,
|
||||
) -> None:
|
||||
connection_id = ("udp", remote_addr, local_addr)
|
||||
if connection_id not in self.manager.connections:
|
||||
reader = udp.DatagramReader()
|
||||
writer = udp.DatagramWriter(transport, remote_addr, reader)
|
||||
handler = ProxyConnectionHandler(
|
||||
ctx.master, reader, writer, ctx.options, self.mode
|
||||
)
|
||||
handler.timeout_watchdog.CONNECTION_TIMEOUT = 20
|
||||
handler.layer = self.make_top_layer(handler.layer.context)
|
||||
handler.layer.context.client.transport_protocol = "udp"
|
||||
handler.layer.context.server.transport_protocol = "udp"
|
||||
|
||||
# pre-register here - we may get datagrams before the task is executed.
|
||||
self.manager.connections[connection_id] = handler
|
||||
asyncio.create_task(self.handle_udp_connection(connection_id, handler))
|
||||
else:
|
||||
handler = self.manager.connections[connection_id]
|
||||
reader = cast(udp.DatagramReader, handler.transports[handler.client].reader)
|
||||
reader.feed_data(data, remote_addr)
|
||||
|
||||
async def handle_udp_connection(self, connection_id: tuple, handler: ProxyConnectionHandler) -> None:
|
||||
with self.manager.register_connection(connection_id, handler):
|
||||
await handler.handle_client()
|
||||
|
||||
|
||||
class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta):
|
||||
_server: asyncio.Server | udp.UdpServer | None = None
|
||||
_listen_addrs: tuple[Address, ...] = tuple()
|
||||
|
||||
@abstractmethod
|
||||
def make_top_layer(self, context: Context) -> Layer:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._server is not None
|
||||
|
@ -202,61 +271,117 @@ class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta):
|
|||
def listen_addrs(self) -> tuple[Address, ...]:
|
||||
return self._listen_addrs
|
||||
|
||||
async def handle_tcp_connection(
|
||||
self,
|
||||
reader: asyncio.StreamReader,
|
||||
writer: asyncio.StreamWriter,
|
||||
) -> None:
|
||||
connection_id = (
|
||||
"tcp",
|
||||
writer.get_extra_info("peername"),
|
||||
writer.get_extra_info("sockname"),
|
||||
)
|
||||
handler = ProxyConnectionHandler(
|
||||
ctx.master, reader, writer, ctx.options, self.mode
|
||||
)
|
||||
handler.layer = self.make_top_layer(handler.layer.context)
|
||||
if isinstance(self.mode, mode_specs.TransparentMode):
|
||||
socket = writer.get_extra_info("socket")
|
||||
try:
|
||||
assert platform.original_addr
|
||||
handler.layer.context.server.address = platform.original_addr(socket)
|
||||
except Exception as e:
|
||||
logger.error(f"Transparent mode failure: {e!r}")
|
||||
return
|
||||
with self.manager.register_connection(connection_id, handler):
|
||||
await handler.handle_client()
|
||||
|
||||
def handle_udp_datagram(
|
||||
self,
|
||||
transport: asyncio.DatagramTransport,
|
||||
data: bytes,
|
||||
remote_addr: Address,
|
||||
local_addr: Address,
|
||||
) -> None:
|
||||
connection_id = ("udp", remote_addr, local_addr)
|
||||
if connection_id not in self.manager.connections:
|
||||
reader = udp.DatagramReader()
|
||||
writer = udp.DatagramWriter(transport, remote_addr, reader)
|
||||
handler = ProxyConnectionHandler(
|
||||
ctx.master, reader, writer, ctx.options, self.mode
|
||||
)
|
||||
handler.timeout_watchdog.CONNECTION_TIMEOUT = 20
|
||||
handler.layer = self.make_top_layer(handler.layer.context)
|
||||
handler.layer.context.client.transport_protocol = "udp"
|
||||
handler.layer.context.server.transport_protocol = "udp"
|
||||
class WireGuardServerInstance(ServerInstance[mode_specs.WireGuardMode]):
|
||||
_server: wg.Server | None = None
|
||||
_listen_addrs: tuple[Address, ...] = tuple()
|
||||
|
||||
# pre-register here - we may get datagrams before the task is executed.
|
||||
self.manager.connections[connection_id] = handler
|
||||
asyncio.create_task(self.handle_udp_connection(connection_id, handler))
|
||||
server_key: str
|
||||
client_key: str
|
||||
|
||||
def make_top_layer(self, context: Context) -> Layer:
|
||||
return layers.modes.TransparentProxy(context)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._server is not None
|
||||
|
||||
async def start(self) -> None:
|
||||
assert self._server is None
|
||||
host = self.mode.listen_host(ctx.options.listen_host)
|
||||
port = self.mode.listen_port(ctx.options.listen_port)
|
||||
|
||||
if self.mode.data:
|
||||
conf_path = Path(self.mode.data).expanduser()
|
||||
else:
|
||||
handler = self.manager.connections[connection_id]
|
||||
reader = cast(udp.DatagramReader, handler.transports[handler.client].reader)
|
||||
reader.feed_data(data, remote_addr)
|
||||
conf_path = Path(ctx.options.confdir).expanduser() / "wireguard.conf"
|
||||
|
||||
async def handle_udp_connection(self, connection_id: tuple, handler: ProxyConnectionHandler) -> None:
|
||||
with self.manager.register_connection(connection_id, handler):
|
||||
await handler.handle_client()
|
||||
try:
|
||||
if not conf_path.exists():
|
||||
conf_path.write_text(json.dumps({
|
||||
"server_key": wg.genkey(),
|
||||
"client_key": wg.genkey(),
|
||||
}, indent=4))
|
||||
|
||||
try:
|
||||
c = json.loads(conf_path.read_text())
|
||||
self.server_key = c["server_key"]
|
||||
self.client_key = c["client_key"]
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid configuration file ({conf_path}): {e}") from e
|
||||
# error early on invalid keys
|
||||
p = wg.pubkey(self.client_key)
|
||||
_ = wg.pubkey(self.server_key)
|
||||
|
||||
self._server = await wg.start_server(
|
||||
host,
|
||||
port,
|
||||
self.server_key,
|
||||
[p],
|
||||
self.wg_handle_tcp_connection,
|
||||
self.wg_handle_udp_datagram,
|
||||
)
|
||||
self._listen_addrs = (self._server.getsockname(),)
|
||||
except Exception as e:
|
||||
self.last_exception = e
|
||||
message = f"{self.mode.description} failed to listen on {host or '*'}:{port} with {e}"
|
||||
raise OSError(message) from e
|
||||
else:
|
||||
self.last_exception = None
|
||||
|
||||
addrs = " and ".join({human.format_address(a) for a in self.listen_addrs})
|
||||
logger.info(f"{self.mode.description} listening at {addrs}.")
|
||||
logger.info(self.client_conf())
|
||||
|
||||
def client_conf(self) -> str | None:
|
||||
if not self._server:
|
||||
return None
|
||||
host = local_ip.get_local_ip() or local_ip.get_local_ip6()
|
||||
port = self.mode.listen_port(ctx.options.listen_port)
|
||||
return textwrap.dedent(f"""
|
||||
------------------------------------------------------------
|
||||
[Interface]
|
||||
PrivateKey = {self.client_key}
|
||||
Address = 10.0.0.1/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = {wg.pubkey(self.server_key)}
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = {host}:{port}
|
||||
------------------------------------------------------------
|
||||
""").strip()
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {
|
||||
"wireguard_conf": self.client_conf(),
|
||||
**super().to_json()
|
||||
}
|
||||
|
||||
async def stop(self) -> None:
|
||||
assert self._server is not None
|
||||
self._server.close()
|
||||
await self._server.wait_closed()
|
||||
self._server = None
|
||||
self.last_exception = None
|
||||
|
||||
addrs = " and ".join({human.format_address(a) for a in self.listen_addrs})
|
||||
logger.info(f"Stopped {self.mode.description} at {addrs}.")
|
||||
|
||||
@property
|
||||
def listen_addrs(self) -> tuple[Address, ...]:
|
||||
return self._listen_addrs
|
||||
|
||||
async def wg_handle_tcp_connection(self, stream: wg.TcpStream) -> None:
|
||||
await self.handle_tcp_connection(stream, stream)
|
||||
|
||||
def wg_handle_udp_datagram(self, data: bytes, remote_addr: Address, local_addr: Address) -> None:
|
||||
transport = WireGuardDatagramTransport(self._server, local_addr, remote_addr)
|
||||
self.handle_udp_datagram(
|
||||
transport,
|
||||
data,
|
||||
remote_addr,
|
||||
local_addr
|
||||
)
|
||||
|
||||
|
||||
class RegularInstance(AsyncioServerInstance[mode_specs.RegularMode]):
|
||||
|
|
|
@ -245,3 +245,13 @@ class DnsMode(ProxyMode):
|
|||
|
||||
def __post_init__(self) -> None:
|
||||
_check_empty(self.data)
|
||||
|
||||
|
||||
class WireGuardMode(ProxyMode):
|
||||
"""Proxy Server based on WireGuard"""
|
||||
description = "WireGuard server"
|
||||
default_port = 51820
|
||||
transport_protocol = UDP
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
pass
|
||||
|
|
|
@ -18,7 +18,9 @@ from contextlib import contextmanager
|
|||
from dataclasses import dataclass
|
||||
from typing import Optional, Union
|
||||
|
||||
import mitmproxy_wireguard as wg
|
||||
from OpenSSL import SSL
|
||||
|
||||
from mitmproxy import http, options as moptions, tls
|
||||
from mitmproxy.proxy.context import Context
|
||||
from mitmproxy.proxy.layers.http import HTTPMode
|
||||
|
@ -78,8 +80,8 @@ class TimeoutWatchdog:
|
|||
@dataclass
|
||||
class ConnectionIO:
|
||||
handler: Optional[asyncio.Task] = None
|
||||
reader: Optional[Union[asyncio.StreamReader, udp.DatagramReader]] = None
|
||||
writer: Optional[Union[asyncio.StreamWriter, udp.DatagramWriter]] = None
|
||||
reader: Optional[Union[asyncio.StreamReader, udp.DatagramReader, wg.TcpStream]] = None
|
||||
writer: Optional[Union[asyncio.StreamWriter, udp.DatagramWriter, wg.TcpStream]] = None
|
||||
|
||||
|
||||
class ConnectionHandler(metaclass=abc.ABCMeta):
|
||||
|
@ -132,6 +134,8 @@ class ConnectionHandler(metaclass=abc.ABCMeta):
|
|||
self.transports[self.client].handler = handler
|
||||
self.server_event(events.Start())
|
||||
await asyncio.wait([handler])
|
||||
if not handler.cancelled() and (e := handler.exception()):
|
||||
self.log(f"mitmproxy has crashed!\n{traceback.format_exception(e)}", logging.ERROR)
|
||||
|
||||
watch.cancel()
|
||||
while self.wakeup_timer:
|
||||
|
@ -407,8 +411,8 @@ class ConnectionHandler(metaclass=abc.ABCMeta):
|
|||
class LiveConnectionHandler(ConnectionHandler, metaclass=abc.ABCMeta):
|
||||
def __init__(
|
||||
self,
|
||||
reader: asyncio.StreamReader,
|
||||
writer: asyncio.StreamWriter,
|
||||
reader: Union[asyncio.StreamReader, wg.TcpStream],
|
||||
writer: Union[asyncio.StreamWriter, wg.TcpStream],
|
||||
options: moptions.Options,
|
||||
mode: mode_specs.ProxyMode,
|
||||
) -> None:
|
||||
|
|
|
@ -69,6 +69,7 @@ exclude =
|
|||
mitmproxy/net/http/message.py
|
||||
mitmproxy/net/http/multipart.py
|
||||
mitmproxy/net/tls.py
|
||||
mitmproxy/net/udp_wireguard.py
|
||||
mitmproxy/options.py
|
||||
mitmproxy/proxy/config.py
|
||||
mitmproxy/proxy/server.py
|
||||
|
|
1
setup.py
1
setup.py
|
@ -82,6 +82,7 @@ setup(
|
|||
"hyperframe>=6.0,<7",
|
||||
"kaitaistruct>=0.10,<0.11",
|
||||
"ldap3>=2.8,<2.10",
|
||||
"mitmproxy_wireguard>=0.1.6,<0.2",
|
||||
"msgpack>=1.0.0, <1.1.0",
|
||||
"passlib>=1.6.5, <1.8",
|
||||
"protobuf>=3.14,<5",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# testing this in isolation makes no sense. See proxy/test_mode_servers.py.
|
|
@ -1,13 +1,15 @@
|
|||
import asyncio
|
||||
import platform
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from mitmproxy import platform
|
||||
import mitmproxy.platform
|
||||
from mitmproxy.addons.proxyserver import Proxyserver
|
||||
from mitmproxy.net import udp
|
||||
from mitmproxy.proxy.mode_servers import DnsInstance, ServerInstance
|
||||
from mitmproxy.proxy.mode_servers import DnsInstance, ServerInstance, WireGuardServerInstance
|
||||
from mitmproxy.proxy.server import ConnectionHandler
|
||||
from mitmproxy.test import taddons
|
||||
|
||||
|
||||
|
@ -23,6 +25,9 @@ def test_make():
|
|||
assert inst.mode.description
|
||||
assert inst.to_json()
|
||||
|
||||
with pytest.raises(ValueError, match="is not a spec for a WireGuardServerInstance server."):
|
||||
WireGuardServerInstance.make("regular", manager)
|
||||
|
||||
|
||||
async def test_last_exception_and_running(monkeypatch):
|
||||
manager = MagicMock()
|
||||
|
@ -33,7 +38,6 @@ async def test_last_exception_and_running(monkeypatch):
|
|||
raise err
|
||||
|
||||
with taddons.context():
|
||||
|
||||
inst1 = ServerInstance.make("regular@127.0.0.1:0", manager)
|
||||
await inst1.start()
|
||||
assert inst1.last_exception is None
|
||||
|
@ -80,9 +84,9 @@ async def test_transparent(failure, monkeypatch, caplog_async):
|
|||
manager = MagicMock()
|
||||
|
||||
if failure:
|
||||
monkeypatch.setattr(platform, "original_addr", None)
|
||||
monkeypatch.setattr(mitmproxy.platform, "original_addr", None)
|
||||
else:
|
||||
monkeypatch.setattr(platform, "original_addr", lambda s: ("address", 42))
|
||||
monkeypatch.setattr(mitmproxy.platform, "original_addr", lambda s: ("address", 42))
|
||||
|
||||
with taddons.context(Proxyserver()) as tctx:
|
||||
tctx.options.connection_strategy = "lazy"
|
||||
|
@ -107,6 +111,91 @@ async def test_transparent(failure, monkeypatch, caplog_async):
|
|||
assert await caplog_async.await_log("Stopped transparent proxy")
|
||||
|
||||
|
||||
async def test_wireguard(tdata, monkeypatch, caplog):
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
async def handle_client(self: ConnectionHandler):
|
||||
t = self.transports[self.client]
|
||||
data = await t.reader.read(65535)
|
||||
t.writer.write(data.upper())
|
||||
await t.writer.drain()
|
||||
t.writer.close()
|
||||
|
||||
monkeypatch.setattr(ConnectionHandler, "handle_client", handle_client)
|
||||
|
||||
system = platform.system()
|
||||
if system == "Linux":
|
||||
test_client_name = "linux-x86_64"
|
||||
elif system == "Darwin":
|
||||
test_client_name = "macos-x86_64"
|
||||
elif system == "Windows":
|
||||
test_client_name = "windows-x86_64.exe"
|
||||
else:
|
||||
return pytest.skip("Unsupported platform for wg-test-client.")
|
||||
|
||||
test_client_path = tdata.path(f"wg-test-client/{test_client_name}")
|
||||
test_conf = tdata.path(f"wg-test-client/test.conf")
|
||||
|
||||
with taddons.context(Proxyserver()):
|
||||
inst = WireGuardServerInstance.make(f"wireguard:{test_conf}@0", MagicMock())
|
||||
|
||||
await inst.start()
|
||||
assert "WireGuard server listening" in caplog.text
|
||||
|
||||
_, port = inst.listen_addrs[0]
|
||||
|
||||
assert inst.is_running
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
test_client_path,
|
||||
str(port),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
|
||||
try:
|
||||
assert proc.returncode == 0
|
||||
except AssertionError:
|
||||
print(stdout)
|
||||
print(stderr)
|
||||
raise
|
||||
|
||||
await inst.stop()
|
||||
assert "Stopped WireGuard server" in caplog.text
|
||||
|
||||
|
||||
async def test_wireguard_generate_conf(tmp_path):
|
||||
with taddons.context(Proxyserver()) as tctx:
|
||||
tctx.options.confdir = str(tmp_path)
|
||||
inst = WireGuardServerInstance.make(f"wireguard@0", MagicMock())
|
||||
assert not inst.client_conf() # should not error.
|
||||
|
||||
await inst.start()
|
||||
|
||||
assert (tmp_path / "wireguard.conf").exists()
|
||||
assert inst.client_conf()
|
||||
assert inst.to_json()["wireguard_conf"]
|
||||
k = inst.server_key
|
||||
|
||||
inst2 = WireGuardServerInstance.make(f"wireguard@0", MagicMock())
|
||||
await inst2.start()
|
||||
assert k == inst2.server_key
|
||||
|
||||
await inst.stop()
|
||||
await inst2.stop()
|
||||
|
||||
|
||||
async def test_wireguard_invalid_conf(tmp_path):
|
||||
with taddons.context(Proxyserver()):
|
||||
# directory instead of filename
|
||||
inst = WireGuardServerInstance.make(f"wireguard:{tmp_path}", MagicMock())
|
||||
|
||||
with pytest.raises(OSError):
|
||||
await inst.start()
|
||||
|
||||
assert "Invalid configuration file" in repr(inst.last_exception)
|
||||
|
||||
|
||||
async def test_tcp_start_error():
|
||||
manager = MagicMock()
|
||||
|
||||
|
|
|
@ -58,6 +58,9 @@ def test_parse_specific_modes():
|
|||
assert ProxyMode.parse("dns")
|
||||
assert ProxyMode.parse("reverse:dns://8.8.8.8")
|
||||
assert ProxyMode.parse("reverse:dtls://127.0.0.1:8004")
|
||||
assert ProxyMode.parse("wireguard")
|
||||
assert ProxyMode.parse("wireguard:foo.conf").data == "foo.conf"
|
||||
assert ProxyMode.parse("wireguard@51821").listen_port() == 51821
|
||||
|
||||
with pytest.raises(ValueError, match="invalid port"):
|
||||
ProxyMode.parse("regular@invalid-port")
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
The mitmproxy_wireguard test client is available under the same license (MIT)
|
||||
as the mitmproxy_wireguard Python package and mitmproxy itself:
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Copyright (c) 2022, Fabio Valentini and Maximilian Hils
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
The test client also contains code from third-party Rust crates, which are
|
||||
available under the following licenses:
|
||||
|
||||
aead v0.5.1: MIT OR Apache-2.0
|
||||
anyhow v1.0.65: MIT OR Apache-2.0
|
||||
base64 v0.13.0: MIT/Apache-2.0
|
||||
bitflags v1.3.2: MIT/Apache-2.0
|
||||
blake2 v0.10.4: MIT OR Apache-2.0
|
||||
block-buffer v0.10.3: MIT OR Apache-2.0
|
||||
boringtun v0.5.2: BSD-3-Clause
|
||||
byteorder v1.4.3: Unlicense OR MIT
|
||||
cfg-if v1.0.0: MIT/Apache-2.0
|
||||
chacha20poly1305 v0.10.1: Apache-2.0 OR MIT
|
||||
chacha20 v0.9.0: Apache-2.0 OR MIT
|
||||
cipher v0.4.3: MIT OR Apache-2.0
|
||||
cpufeatures v0.2.5: MIT OR Apache-2.0
|
||||
crypto-common v0.1.6: MIT OR Apache-2.0
|
||||
curve25519-dalek v3.2.0: BSD-3-Clause
|
||||
digest v0.10.5: MIT OR Apache-2.0
|
||||
digest v0.9.0: MIT OR Apache-2.0
|
||||
generic-array v0.14.6: MIT
|
||||
getrandom v0.1.16: MIT OR Apache-2.0
|
||||
getrandom v0.2.7: MIT OR Apache-2.0
|
||||
hex v0.4.3: MIT OR Apache-2.0
|
||||
hmac v0.12.1: MIT OR Apache-2.0
|
||||
inout v0.1.3: MIT OR Apache-2.0
|
||||
ip_network_table-deps-treebitmap v0.5.0: MIT
|
||||
ip_network_table v0.2.0: BSD-2-Clause
|
||||
ip_network v0.4.1: BSD-2-Clause
|
||||
libc v0.2.132: MIT OR Apache-2.0
|
||||
lock_api v0.4.8: MIT OR Apache-2.0
|
||||
log v0.4.17: MIT OR Apache-2.0
|
||||
managed v0.8.0: 0BSD
|
||||
once_cell v1.14.0: MIT OR Apache-2.0
|
||||
opaque-debug v0.3.0: MIT OR Apache-2.0
|
||||
parking_lot_core v0.9.3: MIT OR Apache-2.0
|
||||
parking_lot v0.12.1: MIT OR Apache-2.0
|
||||
pin-project-lite v0.2.9: Apache-2.0 OR MIT
|
||||
poly1305 v0.8.0: Apache-2.0 OR MIT
|
||||
rand_core v0.5.1: MIT OR Apache-2.0
|
||||
rand_core v0.6.4: MIT OR Apache-2.0
|
||||
ring v0.16.20:
|
||||
scopeguard v1.1.0: MIT/Apache-2.0
|
||||
smallvec v1.9.0: MIT OR Apache-2.0
|
||||
smoltcp v0.8.1: 0BSD
|
||||
spin v0.5.2: MIT
|
||||
subtle v2.4.1: BSD-3-Clause
|
||||
tracing-core v0.1.29: MIT
|
||||
tracing v0.1.36: MIT
|
||||
typenum v1.15.0: MIT OR Apache-2.0
|
||||
universal-hash v0.5.0: MIT OR Apache-2.0
|
||||
untrusted v0.7.1: ISC
|
||||
untrusted v0.9.0: ISC
|
||||
x25519-dalek v2.0.0-pre.1: BSD-3-Clause
|
||||
zeroize v1.5.7: Apache-2.0 OR MIT
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
This list of third-party crates and their licenses was collected for v0.1.6 of
|
||||
the test client by running this command:
|
||||
|
||||
$ cargo tree --prefix none --edges no-build,no-dev,no-proc-macro --format "{p}: {l}" --no-dedupe | sort -u
|
|
@ -0,0 +1,9 @@
|
|||
# mitm-wg-test-client
|
||||
|
||||
This directory contains simple test client binaries built from
|
||||
<https://github.com/decathorpe/mitmproxy_wireguard> version v0.1.6. New versions
|
||||
of the test client binaries are published as release assets on GitHub.
|
||||
|
||||
The test binaries are used for sending WireGuard traffic from userspace in
|
||||
`tests/mitmproxy/proxy/test_mode_servers.py:test_wireguard`.
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"server_key": "EG47ZWjYjr+Y97TQ1A7sVl7Xn3mMWDnvjU/VxU769ls=",
|
||||
"client_key": "qG8b7LI/s+ezngWpXqj5A7Nj988hbGL+eQ8ePki0iHk="
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue