mirror of
https://github.com/mitmproxy/mitmproxy.git
synced 2024-11-22 20:59:45 +00:00
Add tun
proxy mode on Linux (#7278)
Some checks failed
autofix.ci / autofix (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / filename-matching (push) Has been cancelled
CI / mypy (push) Has been cancelled
CI / individual-coverage (push) Has been cancelled
CI / test (macos-latest, 3.13) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.13) (push) Has been cancelled
CI / test (windows-latest, 3.13) (push) Has been cancelled
CI / test-old-dependencies (push) Has been cancelled
CI / build (macos-13, macos-x86_64) (push) Has been cancelled
CI / build (macos-14, macos-arm64) (push) Has been cancelled
CI / build (ubuntu-20.04, linux) (push) Has been cancelled
CI / build (windows-2019, windows) (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-windows-installer (push) Has been cancelled
CI / test-web-ui (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / test-docker (push) Has been cancelled
CI / check (push) Has been cancelled
CI / deploy-docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
Some checks failed
autofix.ci / autofix (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / filename-matching (push) Has been cancelled
CI / mypy (push) Has been cancelled
CI / individual-coverage (push) Has been cancelled
CI / test (macos-latest, 3.13) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.13) (push) Has been cancelled
CI / test (windows-latest, 3.13) (push) Has been cancelled
CI / test-old-dependencies (push) Has been cancelled
CI / build (macos-13, macos-x86_64) (push) Has been cancelled
CI / build (macos-14, macos-arm64) (push) Has been cancelled
CI / build (ubuntu-20.04, linux) (push) Has been cancelled
CI / build (windows-2019, windows) (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-windows-installer (push) Has been cancelled
CI / test-web-ui (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / test-docker (push) Has been cancelled
CI / check (push) Has been cancelled
CI / deploy-docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
* simplify stream handling callback * add `tun` proxy mode from mitmproxy_rs * tun mode: tests++ * [autofix.ci] apply automated fixes * bump mitmproxy_rs * fix bugs * ci: use macOS 13 for builds as 12 is being phased out * test debugging * bump mitmproxy_rs * bump python version in ci, 3.13 is stable now * nits * is unshare to blame? * how about this? * coverage++ * [autofix.ci] apply automated fixes * debüg * debüüg * debüüüg * bump mitmproxy_rs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
cf362f2e85
commit
69f455b962
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@ -44,11 +44,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
py: "3.13-dev"
|
||||
py: "3.13"
|
||||
- os: windows-latest
|
||||
py: "3.13-dev"
|
||||
py: "3.13"
|
||||
- os: macos-latest
|
||||
py: "3.13-dev"
|
||||
py: "3.13"
|
||||
- os: ubuntu-latest
|
||||
py: "3.12"
|
||||
- os: ubuntu-latest
|
||||
@ -100,7 +100,7 @@ jobs:
|
||||
include:
|
||||
- image: macos-14
|
||||
platform: macos-arm64
|
||||
- image: macos-12
|
||||
- image: macos-13
|
||||
platform: macos-x86_64
|
||||
- image: windows-2019
|
||||
platform: windows
|
||||
|
@ -11,6 +11,8 @@
|
||||
([#7242](https://github.com/mitmproxy/mitmproxy/pull/7242), @mhils)
|
||||
- Tighten HTTP detection heuristic to better support custom TCP-based protocols.
|
||||
([#7228](https://github.com/mitmproxy/mitmproxy/pull/7228), @fatanugraha)
|
||||
- Add a `tun` proxy mode that creates a virtual network device on Linux for transparent proxying.
|
||||
([#7278](https://github.com/mitmproxy/mitmproxy/pull/7278), @mhils)
|
||||
- Fix a bug where mitmproxy would incorrectly report that TLS 1.0 and 1.1 are not supported
|
||||
with the current OpenSSL build.
|
||||
([#7241](https://github.com/mitmproxy/mitmproxy/pull/7241), @mhils)
|
||||
|
@ -184,8 +184,11 @@ class ServerInstance(Generic[M], metaclass=ABCMeta):
|
||||
async def handle_stream(
|
||||
self,
|
||||
reader: asyncio.StreamReader | mitmproxy_rs.Stream,
|
||||
writer: asyncio.StreamWriter | mitmproxy_rs.Stream,
|
||||
writer: asyncio.StreamWriter | mitmproxy_rs.Stream | None = None,
|
||||
) -> None:
|
||||
if writer is None:
|
||||
assert isinstance(reader, mitmproxy_rs.Stream)
|
||||
writer = reader
|
||||
handler = ProxyConnectionHandler(
|
||||
ctx.master, reader, writer, ctx.options, self.mode
|
||||
)
|
||||
@ -204,7 +207,8 @@ class ServerInstance(Generic[M], metaclass=ABCMeta):
|
||||
handler.layer.context.client.sockname = original_dst
|
||||
handler.layer.context.server.address = original_dst
|
||||
elif isinstance(
|
||||
self.mode, (mode_specs.WireGuardMode, mode_specs.LocalMode)
|
||||
self.mode,
|
||||
(mode_specs.WireGuardMode, mode_specs.LocalMode, mode_specs.TunMode),
|
||||
): # pragma: no cover on platforms without wg-test-client
|
||||
handler.layer.context.server.address = writer.get_extra_info(
|
||||
"remote_endpoint", handler.layer.context.client.sockname
|
||||
@ -213,9 +217,6 @@ class ServerInstance(Generic[M], metaclass=ABCMeta):
|
||||
with self.manager.register_connection(handler.layer.context.client.id, handler):
|
||||
await handler.handle_client()
|
||||
|
||||
async def handle_udp_stream(self, stream: mitmproxy_rs.Stream) -> None:
|
||||
await self.handle_stream(stream, stream)
|
||||
|
||||
|
||||
class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta):
|
||||
_servers: list[asyncio.Server | mitmproxy_rs.udp.UdpServer]
|
||||
@ -308,14 +309,14 @@ class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta):
|
||||
ipv4 = await mitmproxy_rs.udp.start_udp_server(
|
||||
"0.0.0.0",
|
||||
port,
|
||||
self.handle_udp_stream,
|
||||
self.handle_stream,
|
||||
)
|
||||
servers.append(ipv4)
|
||||
try:
|
||||
ipv6 = await mitmproxy_rs.udp.start_udp_server(
|
||||
"[::]",
|
||||
ipv4.getsockname()[1],
|
||||
self.handle_udp_stream,
|
||||
self.handle_stream,
|
||||
)
|
||||
servers.append(ipv6) # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
@ -325,7 +326,7 @@ class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta):
|
||||
await mitmproxy_rs.udp.start_udp_server(
|
||||
host,
|
||||
port,
|
||||
self.handle_udp_stream,
|
||||
self.handle_stream,
|
||||
)
|
||||
)
|
||||
|
||||
@ -392,8 +393,8 @@ class WireGuardServerInstance(ServerInstance[mode_specs.WireGuardMode]):
|
||||
port,
|
||||
self.server_key,
|
||||
[p],
|
||||
self.wg_handle_stream,
|
||||
self.wg_handle_stream,
|
||||
self.handle_stream,
|
||||
self.handle_stream,
|
||||
)
|
||||
|
||||
conf = self.client_conf()
|
||||
@ -434,11 +435,6 @@ class WireGuardServerInstance(ServerInstance[mode_specs.WireGuardMode]):
|
||||
finally:
|
||||
self._server = None
|
||||
|
||||
async def wg_handle_stream(
|
||||
self, stream: mitmproxy_rs.Stream
|
||||
) -> None: # pragma: no cover on platforms without wg-test-client
|
||||
await self.handle_stream(stream, stream)
|
||||
|
||||
|
||||
class LocalRedirectorInstance(ServerInstance[mode_specs.LocalMode]):
|
||||
_server: ClassVar[mitmproxy_rs.local.LocalRedirector | None] = None
|
||||
@ -460,7 +456,7 @@ class LocalRedirectorInstance(ServerInstance[mode_specs.LocalMode]):
|
||||
stream: mitmproxy_rs.Stream,
|
||||
) -> None:
|
||||
if cls._instance is not None:
|
||||
await cls._instance.handle_stream(stream, stream)
|
||||
await cls._instance.handle_stream(stream)
|
||||
|
||||
async def _start(self) -> None:
|
||||
if self._instance:
|
||||
@ -523,6 +519,47 @@ class DnsInstance(AsyncioServerInstance[mode_specs.DnsMode]):
|
||||
return layers.DNSLayer(context)
|
||||
|
||||
|
||||
class TunInstance(ServerInstance[mode_specs.TunMode]):
|
||||
_server: mitmproxy_rs.tun.TunInterface | None = None
|
||||
listen_addrs = ()
|
||||
|
||||
def make_top_layer(
|
||||
self, context: Context
|
||||
) -> Layer: # pragma: no cover mocked in tests
|
||||
return layers.modes.TransparentProxy(context)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._server is not None
|
||||
|
||||
@property
|
||||
def tun_name(self) -> str | None:
|
||||
if self._server:
|
||||
return self._server.tun_name()
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {"tun_name": self.tun_name, **super().to_json()}
|
||||
|
||||
async def _start(self) -> None:
|
||||
assert self._server is None
|
||||
self._server = await mitmproxy_rs.tun.create_tun_interface(
|
||||
self.handle_stream,
|
||||
self.handle_stream,
|
||||
tun_name=self.mode.data or None,
|
||||
)
|
||||
logger.info(f"TUN interface created: {self._server.tun_name()}")
|
||||
|
||||
async def _stop(self) -> None:
|
||||
assert self._server is not None
|
||||
try:
|
||||
self._server.close()
|
||||
await self._server.wait_closed()
|
||||
finally:
|
||||
self._server = None
|
||||
|
||||
|
||||
# class Http3Instance(AsyncioServerInstance[mode_specs.Http3Mode]):
|
||||
# def make_top_layer(self, context: Context) -> Layer:
|
||||
# return layers.modes.HttpProxy(context)
|
||||
|
@ -23,6 +23,8 @@ Examples:
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
from abc import ABCMeta
|
||||
from abc import abstractmethod
|
||||
@ -302,6 +304,25 @@ class LocalMode(ProxyMode):
|
||||
mitmproxy_rs.local.LocalRedirector.describe_spec(self.data)
|
||||
|
||||
|
||||
class TunMode(ProxyMode):
|
||||
"""A Tun interface."""
|
||||
|
||||
description = "TUN interface"
|
||||
default_port = None
|
||||
transport_protocol = BOTH
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
invalid_tun_name = self.data and (
|
||||
# The Rust side is Linux only for the moment, but eventually we may need this.
|
||||
platform.system() == "Darwin" and not re.match(r"^utun\d+$", self.data)
|
||||
)
|
||||
if invalid_tun_name: # pragma: no cover
|
||||
raise ValueError(
|
||||
f"Invalid tun name: {self.data}. "
|
||||
f"On macOS, the tun name must be the form utunx where x is a number, such as utun3."
|
||||
)
|
||||
|
||||
|
||||
class OsProxyMode(ProxyMode): # pragma: no cover
|
||||
"""Deprecated alias for LocalMode"""
|
||||
|
||||
|
@ -43,7 +43,7 @@ dependencies = [
|
||||
"hyperframe>=6.0,<=6.0.1",
|
||||
"kaitaistruct>=0.10,<=0.10",
|
||||
"ldap3>=2.8,<=2.9.1",
|
||||
"mitmproxy_rs>=0.9.1,<0.10", # relaxed upper bound here: we control this
|
||||
"mitmproxy_rs>=0.10.7,<0.11", # relaxed upper bound here: we control this
|
||||
"msgpack>=1.0.0,<=1.1.0",
|
||||
"passlib>=1.6.5,<=1.7.4",
|
||||
"protobuf>=5.27.2,<=5.28.2",
|
||||
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
|
||||
@ -15,6 +16,10 @@ skip_not_windows = pytest.mark.skipif(
|
||||
os.name != "nt", reason="Skipping due to not Windows"
|
||||
)
|
||||
|
||||
skip_not_linux = pytest.mark.skipif(
|
||||
platform.system() != "Linux", reason="Skipping due to not Linux"
|
||||
)
|
||||
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
s.bind(("::1", 0))
|
||||
|
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import platform
|
||||
import socket
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import Mock
|
||||
@ -7,11 +8,13 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
|
||||
from ...conftest import no_ipv6
|
||||
from ...conftest import skip_not_linux
|
||||
import mitmproxy.platform
|
||||
import mitmproxy_rs
|
||||
from mitmproxy.addons.proxyserver import Proxyserver
|
||||
from mitmproxy.proxy.mode_servers import LocalRedirectorInstance
|
||||
from mitmproxy.proxy.mode_servers import ServerInstance
|
||||
from mitmproxy.proxy.mode_servers import TunInstance
|
||||
from mitmproxy.proxy.mode_servers import WireGuardServerInstance
|
||||
from mitmproxy.proxy.server import ConnectionHandler
|
||||
from mitmproxy.test import taddons
|
||||
@ -130,17 +133,18 @@ async def test_transparent(failure, monkeypatch, caplog_async):
|
||||
assert await caplog_async.await_log("stopped")
|
||||
|
||||
|
||||
async def _echo_server(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()
|
||||
|
||||
|
||||
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)
|
||||
monkeypatch.setattr(ConnectionHandler, "handle_client", _echo_server)
|
||||
|
||||
system = platform.system()
|
||||
if system == "Linux":
|
||||
@ -353,6 +357,56 @@ async def test_dns_start_stop(caplog_async, transport_protocol):
|
||||
assert await caplog_async.await_log("stopped")
|
||||
|
||||
|
||||
@skip_not_linux
|
||||
async def test_tun_mode(monkeypatch, caplog):
|
||||
monkeypatch.setattr(ConnectionHandler, "handle_client", _echo_server)
|
||||
|
||||
with taddons.context(Proxyserver()):
|
||||
inst = TunInstance.make(f"tun", MagicMock())
|
||||
assert inst.tun_name is None
|
||||
try:
|
||||
await inst.start()
|
||||
except RuntimeError as e:
|
||||
if "Operation not permitted" in str(e):
|
||||
return pytest.skip("tun mode test must be run as root")
|
||||
raise
|
||||
assert inst.tun_name
|
||||
assert inst.is_running
|
||||
assert "tun_name" in inst.to_json()
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, inst.tun_name.encode())
|
||||
await asyncio.get_running_loop().sock_connect(s, ("192.0.2.1", 1234))
|
||||
reader, writer = await asyncio.open_connection(sock=s)
|
||||
writer.write(b"hello")
|
||||
await writer.drain()
|
||||
assert await reader.readexactly(5) == b"HELLO"
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
await inst.stop()
|
||||
|
||||
|
||||
async def test_tun_mode_mocked(monkeypatch):
|
||||
tun_interface = Mock()
|
||||
tun_interface.tun_name = lambda: "tun0"
|
||||
tun_interface.wait_closed = AsyncMock()
|
||||
create_tun_interface = AsyncMock(return_value=tun_interface)
|
||||
monkeypatch.setattr(mitmproxy_rs.tun, "create_tun_interface", create_tun_interface)
|
||||
|
||||
inst = TunInstance.make(f"tun", MagicMock())
|
||||
assert not inst.is_running
|
||||
assert inst.tun_name is None
|
||||
|
||||
await inst.start()
|
||||
assert inst.is_running
|
||||
assert inst.tun_name == "tun0"
|
||||
assert inst.to_json()["tun_name"] == "tun0"
|
||||
|
||||
await inst.stop()
|
||||
assert not inst.is_running
|
||||
assert inst.tun_name is None
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def patched_local_redirector(monkeypatch):
|
||||
start_local_redirector = AsyncMock(return_value=Mock())
|
||||
|
@ -70,6 +70,8 @@ def test_parse_specific_modes():
|
||||
assert ProxyMode.parse("wireguard")
|
||||
assert ProxyMode.parse("wireguard:foo.conf").data == "foo.conf"
|
||||
assert ProxyMode.parse("wireguard@51821").listen_port() == 51821
|
||||
assert ProxyMode.parse("tun")
|
||||
assert ProxyMode.parse("tun:utun42")
|
||||
|
||||
assert ProxyMode.parse("local")
|
||||
|
||||
|
@ -31,11 +31,15 @@ async def make() -> str:
|
||||
si2 = ServerInstance.make("reverse:example.com", m.proxyserver)
|
||||
si2.last_exception = RuntimeError("I failed somehow.")
|
||||
si3 = ServerInstance.make("socks5", m.proxyserver)
|
||||
si4 = ServerInstance.make("tun", m.proxyserver)
|
||||
si4._server = Mock()
|
||||
si4._server.tun_name = lambda: "tun0"
|
||||
m.proxyserver.servers._instances.update(
|
||||
{
|
||||
si1.mode: si1,
|
||||
si2.mode: si2,
|
||||
si3.mode: si3,
|
||||
si4.mode: si4,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -41,6 +41,15 @@ export function TBackendState(): Required<BackendState> {
|
||||
"last_exception": null,
|
||||
"listen_addrs": [],
|
||||
"type": "socks5"
|
||||
},
|
||||
"tun": {
|
||||
"description": "TUN interface",
|
||||
"full_spec": "tun",
|
||||
"is_running": true,
|
||||
"last_exception": null,
|
||||
"listen_addrs": [],
|
||||
"tun_name": "tun0",
|
||||
"type": "tun"
|
||||
}
|
||||
},
|
||||
"version": "1.2.3"
|
||||
|
@ -16,6 +16,7 @@ export interface ServerInfo {
|
||||
listen_addrs: [string, number][];
|
||||
type: string;
|
||||
wireguard_conf?: string;
|
||||
tun_name?: string;
|
||||
}
|
||||
|
||||
export interface BackendState {
|
||||
|
Loading…
Reference in New Issue
Block a user