feat: route raw TCP traffic through SOCKS5 proxy
Add create_connection and open_connection helpers to the shared proxy module, covering portcheck, whois, tlscheck, and crtsh live-cert check. UDP-based plugins (dns, blacklist, subdomain) stay direct. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,12 @@ totals (expired/valid), and flag domains still serving expired certs.
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import ssl
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from derp.http import create_connection as _create_connection
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command
|
||||
|
||||
@@ -43,7 +43,7 @@ def check_live_cert(domain: str) -> dict | None:
|
||||
for ctx_factory in (_make_verified_ctx, _make_unverified_ctx):
|
||||
ctx = ctx_factory()
|
||||
try:
|
||||
with socket.create_connection((domain, 443), timeout=10) as sock:
|
||||
with _create_connection((domain, 443), timeout=10) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=domain) as ssock:
|
||||
return ssock.getpeercert()
|
||||
except (OSError, ssl.SSLError):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Plugin: async TCP port scanner (pure stdlib)."""
|
||||
"""Plugin: async TCP port scanner (SOCKS5-proxied)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import ipaddress
|
||||
import time
|
||||
|
||||
from derp.http import open_connection as _open_connection
|
||||
from derp.plugin import command
|
||||
|
||||
_TIMEOUT = 3.0
|
||||
@@ -49,7 +50,7 @@ async def _check_port(host: str, port: int, timeout: float) -> tuple[int, bool,
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port), timeout=timeout,
|
||||
_open_connection(host, port, timeout=timeout), timeout=timeout,
|
||||
)
|
||||
rtt = (time.monotonic() - t0) * 1000
|
||||
writer.close()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Plugin: TLS certificate and cipher inspector (pure stdlib)."""
|
||||
"""Plugin: TLS certificate and cipher inspector (SOCKS5-proxied)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import socket
|
||||
import ssl
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from derp.http import create_connection as _create_connection
|
||||
from derp.plugin import command
|
||||
|
||||
_TIMEOUT = 10
|
||||
@@ -35,7 +35,7 @@ def _inspect(host: str, port: int) -> dict:
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=_TIMEOUT) as sock:
|
||||
with _create_connection((host, port), timeout=_TIMEOUT) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
result["version"] = ssock.version() or ""
|
||||
cipher = ssock.cipher()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Plugin: WHOIS lookup (raw TCP, port 43, pure stdlib)."""
|
||||
"""Plugin: WHOIS lookup (raw TCP, port 43, SOCKS5-proxied)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
|
||||
from derp.http import open_connection as _open_connection
|
||||
from derp.plugin import command
|
||||
|
||||
# Referral servers for common TLDs
|
||||
@@ -51,7 +52,7 @@ def _pick_server(target: str) -> tuple[str, str]:
|
||||
async def _whois(server: str, query: str) -> str:
|
||||
"""Send a WHOIS query and return the response text."""
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(server, 43), timeout=_TIMEOUT,
|
||||
_open_connection(server, 43, timeout=_TIMEOUT), timeout=_TIMEOUT,
|
||||
)
|
||||
try:
|
||||
writer.write(f"{query}\r\n".encode("utf-8"))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Proxy-aware HTTP helpers -- routes outbound traffic through SOCKS5."""
|
||||
"""Proxy-aware HTTP/TCP helpers -- routes outbound traffic through SOCKS5."""
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import ssl
|
||||
import urllib.request
|
||||
|
||||
import socks
|
||||
from socks import SOCKS5
|
||||
from sockshandler import SocksiPyConnectionS, SocksiPyHandler
|
||||
|
||||
@@ -46,3 +48,37 @@ def build_opener(*handlers, context=None):
|
||||
"""Proxy-aware drop-in for urllib.request.build_opener."""
|
||||
proxy = _ProxyHandler(context=context)
|
||||
return urllib.request.build_opener(proxy, *handlers)
|
||||
|
||||
|
||||
def create_connection(address, *, timeout=None):
|
||||
"""SOCKS5-proxied drop-in for socket.create_connection.
|
||||
|
||||
Returns a connected socksocket (usable as context manager).
|
||||
"""
|
||||
host, port = address
|
||||
sock = socks.socksocket()
|
||||
sock.set_proxy(SOCKS5, _PROXY_ADDR, _PROXY_PORT, rdns=True)
|
||||
if timeout is not None:
|
||||
sock.settimeout(timeout)
|
||||
sock.connect((host, port))
|
||||
return sock
|
||||
|
||||
|
||||
async def open_connection(host, port, *, timeout=None):
|
||||
"""SOCKS5-proxied drop-in for asyncio.open_connection.
|
||||
|
||||
SOCKS5 handshake runs in a thread executor; returns (reader, writer).
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _connect():
|
||||
sock = socks.socksocket()
|
||||
sock.set_proxy(SOCKS5, _PROXY_ADDR, _PROXY_PORT, rdns=True)
|
||||
if timeout is not None:
|
||||
sock.settimeout(timeout)
|
||||
sock.connect((host, port))
|
||||
sock.setblocking(False)
|
||||
return sock
|
||||
|
||||
sock = await loop.run_in_executor(None, _connect)
|
||||
return await asyncio.open_connection(sock=sock)
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
"""Tests for the SOCKS5 proxy HTTP module."""
|
||||
"""Tests for the SOCKS5 proxy HTTP/TCP module."""
|
||||
|
||||
import ssl
|
||||
import urllib.request
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import socks
|
||||
from socks import SOCKS5
|
||||
|
||||
from derp.http import _PROXY_ADDR, _PROXY_PORT, _ProxyHandler, build_opener
|
||||
from derp.http import (
|
||||
_PROXY_ADDR,
|
||||
_PROXY_PORT,
|
||||
_ProxyHandler,
|
||||
build_opener,
|
||||
create_connection,
|
||||
)
|
||||
|
||||
|
||||
class TestProxyHandler:
|
||||
@@ -57,3 +65,42 @@ class TestBuildOpener:
|
||||
opener = build_opener(context=ctx)
|
||||
proxy = [h for h in opener.handlers if isinstance(h, _ProxyHandler)][0]
|
||||
assert proxy._ssl_context is ctx
|
||||
|
||||
|
||||
class TestCreateConnection:
|
||||
@patch("derp.http.socks.socksocket")
|
||||
def test_sets_socks5_proxy(self, mock_cls):
|
||||
sock = MagicMock()
|
||||
mock_cls.return_value = sock
|
||||
create_connection(("example.com", 443), timeout=5)
|
||||
sock.set_proxy.assert_called_once_with(
|
||||
SOCKS5, _PROXY_ADDR, _PROXY_PORT, rdns=True,
|
||||
)
|
||||
|
||||
@patch("derp.http.socks.socksocket")
|
||||
def test_connects_to_target(self, mock_cls):
|
||||
sock = MagicMock()
|
||||
mock_cls.return_value = sock
|
||||
create_connection(("example.com", 443))
|
||||
sock.connect.assert_called_once_with(("example.com", 443))
|
||||
|
||||
@patch("derp.http.socks.socksocket")
|
||||
def test_sets_timeout(self, mock_cls):
|
||||
sock = MagicMock()
|
||||
mock_cls.return_value = sock
|
||||
create_connection(("example.com", 80), timeout=7)
|
||||
sock.settimeout.assert_called_once_with(7)
|
||||
|
||||
@patch("derp.http.socks.socksocket")
|
||||
def test_no_timeout_when_none(self, mock_cls):
|
||||
sock = MagicMock()
|
||||
mock_cls.return_value = sock
|
||||
create_connection(("example.com", 80))
|
||||
sock.settimeout.assert_not_called()
|
||||
|
||||
@patch("derp.http.socks.socksocket")
|
||||
def test_returns_socket(self, mock_cls):
|
||||
sock = MagicMock()
|
||||
mock_cls.return_value = sock
|
||||
result = create_connection(("example.com", 443))
|
||||
assert result is sock
|
||||
|
||||
Reference in New Issue
Block a user