diff --git a/compose.yaml b/compose.yaml index 3ef67ae..b326bfd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,6 +20,8 @@ services: environment: GATEWAY_IP: "${GATEWAY_IP:-192.168.20.30}" REDIRECT_URL: "${GUIDE_REDIRECT_URL:-http://192.168.20.30:8000/}" + FORWARDED_DOMAINS: "${FORWARDED_DOMAINS:-homebrew.page,github.com,release-assets.githubusercontent.com}" + UPSTREAM_DNS: "${UPSTREAM_DNS:-1.1.1.1}" ports: - "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/udp" - "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/tcp" diff --git a/docker/guide_gateway.py b/docker/guide_gateway.py index cdfbb7d..07a0882 100644 --- a/docker/guide_gateway.py +++ b/docker/guide_gateway.py @@ -15,6 +15,17 @@ HTTPS_PORT = int(os.environ.get("HTTPS_PORT", "8443")) GATEWAY_IP = ipaddress.IPv4Address(os.environ.get("GATEWAY_IP", "192.168.20.30")) REDIRECT_URL = os.environ.get("REDIRECT_URL", f"http://{GATEWAY_IP}:8000/") TARGET_DOMAIN = "manuals.playstation.net" +# Exact names forwarded to the upstream resolver (homebrew store: catalog + downloads). +FORWARDED_DOMAINS = frozenset( + name.strip().lower() + for name in os.environ.get( + "FORWARDED_DOMAINS", + "homebrew.page,github.com,release-assets.githubusercontent.com", + ).split(",") + if name.strip() +) +UPSTREAM_DNS = str(ipaddress.ip_address(os.environ.get("UPSTREAM_DNS", "1.1.1.1"))) +UPSTREAM_TIMEOUT = 3.0 CERTIFICATE = "/srv/certificates/manuals.crt" PRIVATE_KEY = "/srv/certificates/manuals.key" @@ -43,7 +54,54 @@ def parse_question(packet): return ".".join(labels).lower(), qtype, qclass, offset + 4 -def dns_response(packet): +def error_response(packet, rcode): + transaction_id, request_flags = struct.unpack_from("!HH", packet, 0) + response_flags = 0x8000 | (request_flags & 0x0100) | rcode + try: + _, _, _, question_end = parse_question(packet) + question = packet[12:question_end] + question_count = 1 + except (UnicodeDecodeError, ValueError): + question = b"" + question_count = 0 + return ( + struct.pack("!HHHHHH", transaction_id, response_flags, question_count, 0, 0, 0) + + question + ) + + +def receive_exactly(connection, wanted): + chunks = [] + received = 0 + while received < wanted: + chunk = connection.recv(wanted - received) + if not chunk: + raise OSError("upstream DNS connection closed early") + chunks.append(chunk) + received += len(chunk) + return b"".join(chunks) + + +def forward_query(packet, use_tcp=False): + family = socket.AF_INET6 if ":" in UPSTREAM_DNS else socket.AF_INET + socket_type = socket.SOCK_STREAM if use_tcp else socket.SOCK_DGRAM + with socket.socket(family, socket_type) as upstream: + upstream.settimeout(UPSTREAM_TIMEOUT) + upstream.connect((UPSTREAM_DNS, 53)) + if use_tcp: + upstream.sendall(struct.pack("!H", len(packet)) + packet) + response_length = struct.unpack("!H", receive_exactly(upstream, 2))[0] + response = receive_exactly(upstream, response_length) + else: + upstream.send(packet) + response = upstream.recv(65535) + + if len(response) < 12 or response[:2] != packet[:2]: + raise ValueError("invalid upstream DNS response") + return response + + +def dns_response(packet, use_tcp=False): if len(packet) < 12: return b"" @@ -57,6 +115,13 @@ def dns_response(packet): return struct.pack("!HHHHHH", transaction_id, response_flags | 1, 0, 0, 0, 0) question = packet[12:question_end] + if name in FORWARDED_DOMAINS: + try: + return forward_query(packet, use_tcp=use_tcp) + except (OSError, ValueError) as error: + print(f"DNS upstream failure for {name}: {error}", flush=True) + return error_response(packet, 2) # SERVFAIL + if name != TARGET_DOMAIN: return struct.pack("!HHHHHH", transaction_id, response_flags | 3, 1, 0, 0, 0) + question @@ -109,20 +174,15 @@ class TCPDNSHandler(socketserver.BaseRequestHandler): print(f"DNS TCP {self.client_address[0]} {name} type={qtype}", flush=True) except (UnicodeDecodeError, ValueError): print(f"DNS TCP {self.client_address[0]} malformed", flush=True) - response = dns_response(packet) + response = dns_response(packet, use_tcp=True) if response: self.request.sendall(struct.pack("!H", len(response)) + response) def _receive_exactly(self, wanted): - chunks = [] - received = 0 - while received < wanted: - chunk = self.request.recv(wanted - received) - if not chunk: - return None - chunks.append(chunk) - received += len(chunk) - return b"".join(chunks) + try: + return receive_exactly(self.request, wanted) + except OSError: + return None class RedirectHandler(http.server.BaseHTTPRequestHandler): @@ -187,7 +247,8 @@ if __name__ == "__main__": serve(https, "https-redirect") print( - f"PS5 guide gateway: {TARGET_DOMAIN} -> {GATEWAY_IP}; redirect -> {REDIRECT_URL}", + f"PS5 guide gateway: {TARGET_DOMAIN} -> {GATEWAY_IP}; redirect -> {REDIRECT_URL}; " + f"forward {','.join(sorted(FORWARDED_DOMAINS))} -> {UPSTREAM_DNS}", flush=True, ) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 932eb4e..f39b2e2 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -69,9 +69,11 @@ kadinho - restart policy: `unless-stopped`. The same Compose project also runs `ps5-guide-gateway`, bound specifically to -`192.168.20.30` on DNS UDP/TCP 53 and HTTP/HTTPS TCP 80/443. It resolves only -`manuals.playstation.net`, returns NXDOMAIN for every other name, and redirects the -User Guide to `http://192.168.20.30:8000/`. +`192.168.20.30` on DNS UDP/TCP 53 and HTTP/HTTPS TCP 80/443. It resolves +`manuals.playstation.net` locally and forwards only `homebrew.page`, `github.com` +and `release-assets.githubusercontent.com` to the configured upstream resolver +(`1.1.1.1` by default). Every other name receives NXDOMAIN. The HTTP/HTTPS gateway +redirects the User Guide to `http://192.168.20.30:8000/`. Port `8080` on the host is already used by `unifi-network-application`; do not move this service there. At the last verification, the Relapse container was healthy, @@ -169,9 +171,12 @@ the local container. The local gateway was therefore added. Configure the PS5 wi primary DNS `192.168.20.30` and secondary DNS `0.0.0.0`, then restart the console and accept the self-signed certificate warning in the User Guide. -The gateway intentionally provides no recursive DNS forwarding. Only -`manuals.playstation.net` resolves; all other names receive NXDOMAIN. This blocks -PSN and update resolution and keeps the setup independent of a public DNS operator. +The gateway provides allowlisted recursive DNS forwarding only for `homebrew.page`, +`github.com` and `release-assets.githubusercontent.com`. Only those names and +`manuals.playstation.net` resolve; all other names receive NXDOMAIN. This permits +the homebrew store catalog and HTTPS release downloads while continuing to block +PSN and update resolution. The PS5 sends DNS only to `net-utils01`; that host must +be allowed to reach the configured upstream resolver on UDP/TCP 53. The PS5 was previously identified as `192.168.10.26`; requests from `192.168.10.6` seen at that time were from a different client. Those addresses are historical and must not be used as the current payload destination. diff --git a/docs/PS5_SETUP.md b/docs/PS5_SETUP.md index ae4d74c..8b75307 100644 --- a/docs/PS5_SETUP.md +++ b/docs/PS5_SETUP.md @@ -137,8 +137,11 @@ O Compose inclui um DNS controlado no `net-utils01` que: - bloqueie domínios PlayStation/PSN e servidores de atualização; - responda `manuals.playstation.net` com `192.168.20.30`; -- devolva `NXDOMAIN` para todos os restantes nomes, bloqueando PSN, updates, - telemetria e acesso geral à Internet por DNS; +- encaminhe apenas `homebrew.page`, `github.com` e + `release-assets.githubusercontent.com` para o resolvedor configurado, permitindo + o catálogo, ícones e downloads HTTPS do homebrew store; +- devolva `NXDOMAIN` para todos os restantes nomes, mantendo PSN, updates e + telemetria bloqueados por DNS; - registe as consultas nos logs do container; - apresente um gateway HTTPS local e redirecione o browser para o exploit em `http://192.168.20.30:8000/`. @@ -160,6 +163,17 @@ definir `GATEWAY_BIND_IP`, `GATEWAY_IP` e `GUIDE_REDIRECT_URL` antes de executar Compose. Também é possível bloquear o acesso WAN da PS5 no router, mantendo apenas a comunicação LAN; esta continua a ser uma camada adicional mais forte do que DNS. +O resolvedor upstream predefinido é `1.1.1.1`. Pode ser substituído por um endereço +DNS controlado na rede através de `UPSTREAM_DNS`; o valor deve ser um endereço IP, +não um hostname. A firewall deve permitir UDP e TCP `53` de `net-utils01` para esse +resolvedor. A consola continua a precisar apenas de UDP/TCP `53` para +`192.168.20.30`; não deve consultar diretamente um DNS público. + +O acesso WAN da consola pode ficar limitado a TCP `443` para os três domínios +permitidos. Não são necessárias regras WAN de entrada. Se faltarem ícones, consultar +os logs da firewall: um URL de ícone apontado pelo catálogo para outro domínio será +bloqueado até esse destino ser revisto e acrescentado explicitamente. + ## Abrir pelo Guia do Utilizador Com um DNS que redirecione o manual: