Allow homebrew store DNS lookups
This commit is contained in:
@@ -20,6 +20,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
GATEWAY_IP: "${GATEWAY_IP:-192.168.20.30}"
|
GATEWAY_IP: "${GATEWAY_IP:-192.168.20.30}"
|
||||||
REDIRECT_URL: "${GUIDE_REDIRECT_URL:-http://192.168.20.30:8000/}"
|
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:
|
ports:
|
||||||
- "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/udp"
|
- "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/udp"
|
||||||
- "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/tcp"
|
- "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/tcp"
|
||||||
|
|||||||
+72
-11
@@ -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"))
|
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/")
|
REDIRECT_URL = os.environ.get("REDIRECT_URL", f"http://{GATEWAY_IP}:8000/")
|
||||||
TARGET_DOMAIN = "manuals.playstation.net"
|
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"
|
CERTIFICATE = "/srv/certificates/manuals.crt"
|
||||||
PRIVATE_KEY = "/srv/certificates/manuals.key"
|
PRIVATE_KEY = "/srv/certificates/manuals.key"
|
||||||
|
|
||||||
@@ -43,7 +54,54 @@ def parse_question(packet):
|
|||||||
return ".".join(labels).lower(), qtype, qclass, offset + 4
|
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:
|
if len(packet) < 12:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
@@ -57,6 +115,13 @@ def dns_response(packet):
|
|||||||
return struct.pack("!HHHHHH", transaction_id, response_flags | 1, 0, 0, 0, 0)
|
return struct.pack("!HHHHHH", transaction_id, response_flags | 1, 0, 0, 0, 0)
|
||||||
|
|
||||||
question = packet[12:question_end]
|
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:
|
if name != TARGET_DOMAIN:
|
||||||
return struct.pack("!HHHHHH", transaction_id, response_flags | 3, 1, 0, 0, 0) + question
|
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)
|
print(f"DNS TCP {self.client_address[0]} {name} type={qtype}", flush=True)
|
||||||
except (UnicodeDecodeError, ValueError):
|
except (UnicodeDecodeError, ValueError):
|
||||||
print(f"DNS TCP {self.client_address[0]} malformed", flush=True)
|
print(f"DNS TCP {self.client_address[0]} malformed", flush=True)
|
||||||
response = dns_response(packet)
|
response = dns_response(packet, use_tcp=True)
|
||||||
if response:
|
if response:
|
||||||
self.request.sendall(struct.pack("!H", len(response)) + response)
|
self.request.sendall(struct.pack("!H", len(response)) + response)
|
||||||
|
|
||||||
def _receive_exactly(self, wanted):
|
def _receive_exactly(self, wanted):
|
||||||
chunks = []
|
try:
|
||||||
received = 0
|
return receive_exactly(self.request, wanted)
|
||||||
while received < wanted:
|
except OSError:
|
||||||
chunk = self.request.recv(wanted - received)
|
|
||||||
if not chunk:
|
|
||||||
return None
|
return None
|
||||||
chunks.append(chunk)
|
|
||||||
received += len(chunk)
|
|
||||||
return b"".join(chunks)
|
|
||||||
|
|
||||||
|
|
||||||
class RedirectHandler(http.server.BaseHTTPRequestHandler):
|
class RedirectHandler(http.server.BaseHTTPRequestHandler):
|
||||||
@@ -187,7 +247,8 @@ if __name__ == "__main__":
|
|||||||
serve(https, "https-redirect")
|
serve(https, "https-redirect")
|
||||||
|
|
||||||
print(
|
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,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+11
-6
@@ -69,9 +69,11 @@ kadinho <[email protected]>
|
|||||||
- restart policy: `unless-stopped`.
|
- restart policy: `unless-stopped`.
|
||||||
|
|
||||||
The same Compose project also runs `ps5-guide-gateway`, bound specifically to
|
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
|
`192.168.20.30` on DNS UDP/TCP 53 and HTTP/HTTPS TCP 80/443. It resolves
|
||||||
`manuals.playstation.net`, returns NXDOMAIN for every other name, and redirects the
|
`manuals.playstation.net` locally and forwards only `homebrew.page`, `github.com`
|
||||||
User Guide to `http://192.168.20.30:8000/`.
|
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
|
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,
|
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
|
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.
|
accept the self-signed certificate warning in the User Guide.
|
||||||
|
|
||||||
The gateway intentionally provides no recursive DNS forwarding. Only
|
The gateway provides allowlisted recursive DNS forwarding only for `homebrew.page`,
|
||||||
`manuals.playstation.net` resolves; all other names receive NXDOMAIN. This blocks
|
`github.com` and `release-assets.githubusercontent.com`. Only those names and
|
||||||
PSN and update resolution and keeps the setup independent of a public DNS operator.
|
`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
|
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
|
`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.
|
are historical and must not be used as the current payload destination.
|
||||||
|
|||||||
+16
-2
@@ -137,8 +137,11 @@ O Compose inclui um DNS controlado no `net-utils01` que:
|
|||||||
|
|
||||||
- bloqueie domínios PlayStation/PSN e servidores de atualização;
|
- bloqueie domínios PlayStation/PSN e servidores de atualização;
|
||||||
- responda `manuals.playstation.net` com `192.168.20.30`;
|
- responda `manuals.playstation.net` com `192.168.20.30`;
|
||||||
- devolva `NXDOMAIN` para todos os restantes nomes, bloqueando PSN, updates,
|
- encaminhe apenas `homebrew.page`, `github.com` e
|
||||||
telemetria e acesso geral à Internet por DNS;
|
`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;
|
- registe as consultas nos logs do container;
|
||||||
- apresente um gateway HTTPS local e redirecione o browser para o exploit em
|
- apresente um gateway HTTPS local e redirecione o browser para o exploit em
|
||||||
`http://192.168.20.30:8000/`.
|
`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
|
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.
|
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
|
## Abrir pelo Guia do Utilizador
|
||||||
|
|
||||||
Com um DNS que redirecione o manual:
|
Com um DNS que redirecione o manual:
|
||||||
|
|||||||
Reference in New Issue
Block a user