Ctrl K
ComunidadGuías…

dns_proxy.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3

import argparse
import ipaddress
import select
import socket
import socketserver
import subprocess
import sys
import threading
import time

from atomic_io import atomic_write_text


DNS_SERVER = '1.1.1.1'
MAX_HEADER_BYTES = 16384
RESOLUTION_TTL = 300


class Resolver:
    def __init__(self):
        self.cache = {}
        self.lock = threading.Lock()

    def resolve(self, hostname):
        now = time.monotonic()
        with self.lock:
            cached = self.cache.get(hostname)
            if cached and cached[1] > now:
                return cached[0]

        result = subprocess.run(
            ['dig', '+short', '@{}'.format(DNS_SERVER), hostname, 'A'],
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=15,
            check=False,
        )
        if result.returncode != 0:
            raise RuntimeError(
                'DNS fallback failed for {}: {}'.format(hostname, result.stderr.strip())
            )

        addresses = []
        for line in result.stdout.splitlines():
            candidate = line.strip()
            try:
                if ipaddress.ip_address(candidate).version == 4:
                    addresses.append(candidate)
            except ValueError:
                continue
        if not addresses:
            raise RuntimeError('DNS fallback returned no IPv4 address for {}'.format(hostname))

        address = addresses[0]
        with self.lock:
            self.cache[hostname] = (address, now + RESOLUTION_TTL)
        return address


resolver = Resolver()


class ThreadingProxy(socketserver.ThreadingTCPServer):
    allow_reuse_address = True
    daemon_threads = True


class ConnectHandler(socketserver.BaseRequestHandler):
    def read_headers(self):
        data = b''
        while b'\r\n\r\n' not in data:
            chunk = self.request.recv(4096)
            if not chunk:
                raise RuntimeError('client disconnected before sending proxy headers')
            data += chunk
            if len(data) > MAX_HEADER_BYTES:
                raise RuntimeError('proxy request headers are too large')
        return data

    def handle(self):
        remote = None
        try:
            headers = self.read_headers()
            request_line = headers.split(b'\r\n', 1)[0].decode('ascii', errors='strict')
            method, authority, _version = request_line.split(' ', 2)
            if method.upper() != 'CONNECT':
                self.request.sendall(b'HTTP/1.1 405 Method Not Allowed\r\n\r\n')
                return

            hostname, port_text = authority.rsplit(':', 1)
            port = int(port_text)
            if port != 443:
                raise RuntimeError('only HTTPS port 443 is allowed')
            if not hostname or any(
                character not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-'
                for character in hostname
            ):
                raise RuntimeError('invalid proxy hostname')

            address = resolver.resolve(hostname)
            remote = socket.create_connection((address, port), timeout=20)
            self.request.sendall(b'HTTP/1.1 200 Connection Established\r\n\r\n')
            self.request.settimeout(None)
            remote.settimeout(None)

            sockets = [self.request, remote]
            while True:
                readable, _writable, exceptional = select.select(sockets, [], sockets, 60)
                if exceptional or not readable:
                    return
                for source in readable:
                    target = remote if source is self.request else self.request
                    chunk = source.recv(65536)
                    if not chunk:
                        return
                    target.sendall(chunk)
        except Exception as error:
            print('DNS proxy connection failed: {}'.format(error), file=sys.stderr)
            try:
                self.request.sendall(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
            except OSError:
                pass
        finally:
            if remote is not None:
                remote.close()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--port-file', required=True)
    args = parser.parse_args()

    with ThreadingProxy(('127.0.0.1', 0), ConnectHandler) as server:
        port = server.server_address[1]
        atomic_write_text(args.port_file, '{}\n'.format(port), default_mode=0o600)
        print('DNS fallback proxy listening on 127.0.0.1:{}'.format(port), file=sys.stderr)
        server.serve_forever(poll_interval=0.5)
    return 0


if __name__ == '__main__':
    raise SystemExit(main())