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 asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
import ssl
|
import ssl
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from derp.http import create_connection as _create_connection
|
||||||
from derp.http import urlopen as _urlopen
|
from derp.http import urlopen as _urlopen
|
||||||
from derp.plugin import command
|
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):
|
for ctx_factory in (_make_verified_ctx, _make_unverified_ctx):
|
||||||
ctx = ctx_factory()
|
ctx = ctx_factory()
|
||||||
try:
|
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:
|
with ctx.wrap_socket(sock, server_hostname=domain) as ssock:
|
||||||
return ssock.getpeercert()
|
return ssock.getpeercert()
|
||||||
except (OSError, ssl.SSLError):
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from derp.http import open_connection as _open_connection
|
||||||
from derp.plugin import command
|
from derp.plugin import command
|
||||||
|
|
||||||
_TIMEOUT = 3.0
|
_TIMEOUT = 3.0
|
||||||
@@ -49,7 +50,7 @@ async def _check_port(host: str, port: int, timeout: float) -> tuple[int, bool,
|
|||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
try:
|
try:
|
||||||
_, writer = await asyncio.wait_for(
|
_, 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
|
rtt = (time.monotonic() - t0) * 1000
|
||||||
writer.close()
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import socket
|
|
||||||
import ssl
|
import ssl
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from derp.http import create_connection as _create_connection
|
||||||
from derp.plugin import command
|
from derp.plugin import command
|
||||||
|
|
||||||
_TIMEOUT = 10
|
_TIMEOUT = 10
|
||||||
@@ -35,7 +35,7 @@ def _inspect(host: str, port: int) -> dict:
|
|||||||
ctx.verify_mode = ssl.CERT_NONE
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
try:
|
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:
|
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
||||||
result["version"] = ssock.version() or ""
|
result["version"] = ssock.version() or ""
|
||||||
cipher = ssock.cipher()
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
|
from derp.http import open_connection as _open_connection
|
||||||
from derp.plugin import command
|
from derp.plugin import command
|
||||||
|
|
||||||
# Referral servers for common TLDs
|
# 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:
|
async def _whois(server: str, query: str) -> str:
|
||||||
"""Send a WHOIS query and return the response text."""
|
"""Send a WHOIS query and return the response text."""
|
||||||
reader, writer = await asyncio.wait_for(
|
reader, writer = await asyncio.wait_for(
|
||||||
asyncio.open_connection(server, 43), timeout=_TIMEOUT,
|
_open_connection(server, 43, timeout=_TIMEOUT), timeout=_TIMEOUT,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
writer.write(f"{query}\r\n".encode("utf-8"))
|
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 socket
|
||||||
import ssl
|
import ssl
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
import socks
|
||||||
from socks import SOCKS5
|
from socks import SOCKS5
|
||||||
from sockshandler import SocksiPyConnectionS, SocksiPyHandler
|
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-aware drop-in for urllib.request.build_opener."""
|
||||||
proxy = _ProxyHandler(context=context)
|
proxy = _ProxyHandler(context=context)
|
||||||
return urllib.request.build_opener(proxy, *handlers)
|
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 ssl
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import socks
|
||||||
from socks import SOCKS5
|
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:
|
class TestProxyHandler:
|
||||||
@@ -57,3 +65,42 @@ class TestBuildOpener:
|
|||||||
opener = build_opener(context=ctx)
|
opener = build_opener(context=ctx)
|
||||||
proxy = [h for h in opener.handlers if isinstance(h, _ProxyHandler)][0]
|
proxy = [h for h in opener.handlers if isinstance(h, _ProxyHandler)][0]
|
||||||
assert proxy._ssl_context is ctx
|
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