修复代理 Fake-IP 导致 API 任务恢复查询和下载失败

This commit is contained in:
magicwang1111
2026-10-28 11:53:34 +08:00
parent 0cfaf01747
commit 4ef92fe7b2
5 changed files with 79 additions and 5 deletions
+4
View File
@@ -214,6 +214,10 @@ internal signing secret, and shared output directory across restarts. The secret
needed to decrypt pending query credentials; terminal checkpoints discard credentials.
Multiple workers must share these paths and support the recovery file lock.
Recovery queries reuse the original API endpoint and system proxy routing. When a
configured HTTP(S) proxy uses Fake-IP DNS, media address validation checks the
hostname through AliDNS over HTTPS; private addresses remain blocked.
## API Access
Authenticated API calls can include either:
+2 -1
View File
@@ -183,7 +183,8 @@ class ApiRecoveryTests(unittest.TestCase):
response.__enter__.return_value = response
response.status_code = 200
response.json.return_value = {"status": "running"}
with patch.object(recovery, "_validate_public_http_url"), patch.object(recovery.requests, "request", return_value=response) as request:
# A local proxy may resolve public API hosts to benchmark-range Fake-IP addresses.
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("198.18.0.83", 443))]), patch.object(recovery.requests, "request", return_value=response) as request:
self.manager._query(row)
args, kwargs = request.call_args
self.assertEqual(("GET", "https://api.example/api/v3/contents/generations/tasks/task-1"), args)
+50
View File
@@ -0,0 +1,50 @@
import io
import json
import unittest
from unittest.mock import patch
from test_cloud_archive import cloud
class RemoteAddressTests(unittest.TestCase):
def validate(self, address, *, host="cdn.example", proxy=True, public_answer="8.8.8.8"):
response = io.BytesIO(json.dumps({"Status": 0, "Answer": [
{"type": 1, "data": public_answer},
]}).encode())
with patch.object(cloud.socket, "getaddrinfo", return_value=[(2, 1, 6, "", (address, 443))]), \
patch.object(cloud.urllib.request, "getproxies", return_value={"https": "http://127.0.0.1:7897"} if proxy else {}), \
patch.object(cloud.urllib.request, "proxy_bypass", return_value=False), \
patch.object(cloud.urllib.request, "urlopen", return_value=response) as lookup:
cloud._validate_public_http_url("https://" + host + "/video.mp4?signature=private")
return lookup.call_args_list
def test_proxy_fake_ip_validates_public_dns_without_sending_signed_url(self):
calls = self.validate("198.18.0.86")
self.assertEqual(1, len(calls))
url = calls[0].args[0]
self.assertIn("name=cdn.example", url)
self.assertNotIn("signature", url)
def test_public_address_needs_no_extra_lookup(self):
self.assertEqual([], self.validate("8.8.8.8"))
def test_private_loopback_and_metadata_addresses_are_rejected(self):
for address in ("127.0.0.1", "10.0.0.1", "169.254.169.254", "::1"):
with self.subTest(address=address), self.assertRaises(ValueError):
self.validate(address)
def test_fake_ip_without_proxy_is_rejected(self):
with self.assertRaises(ValueError):
self.validate("198.18.0.86", proxy=False)
def test_literal_fake_ip_is_rejected_even_with_proxy(self):
with self.assertRaises(ValueError):
self.validate("198.18.0.86", host="198.18.0.86")
def test_private_public_dns_answer_is_rejected(self):
with self.assertRaises(ValueError):
self.validate("198.18.0.86", public_answer="10.0.0.1")
if __name__ == "__main__":
unittest.main()
+3 -2
View File
@@ -17,7 +17,7 @@ from cryptography.fernet import Fernet
from filelock import FileLock, Timeout
from .api_audit import suspend_api_audit
from .cloud_archive import download_remote, _validate_public_http_url
from .cloud_archive import download_remote
from .recovery_providers import submission, result
@@ -105,7 +105,8 @@ class ApiRecoveryManager:
logger.error("API result recovery iteration failed; retrying")
def _query(self, row):
_validate_public_http_url(row["query_url"])
# This endpoint comes from the authenticated submission, not returned media.
# Use the original client's proxy/DNS routing; Fake-IP is not the API's address.
authorization = self.cipher.decrypt(row["credentials"]).decode()
with requests.request(
row["query_method"], row["query_url"],
+20 -2
View File
@@ -894,8 +894,26 @@ def _validate_public_http_url(url: str) -> None:
addresses = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
except socket.gaierror as error:
raise ValueError(f"Unable to resolve artifact host: {error}") from error
for address in addresses:
ip = ipaddress.ip_address(address[4][0])
ips = [ipaddress.ip_address(address[4][0]) for address in addresses]
fake_network = ipaddress.ip_network("198.18.0.0/15")
if any(ip in fake_network for ip in ips):
# Proxy Fake-IP is a routing token. Validate the hostname's public DNS
# answer instead; never allow literal Fake-IP URLs or private answers.
try:
ipaddress.ip_address(parsed.hostname)
literal = True
except ValueError:
literal = False
proxied = urllib.request.getproxies().get(parsed.scheme) and not urllib.request.proxy_bypass(parsed.hostname)
if not literal and proxied and all(ip in fake_network or ip.is_global for ip in ips):
query = urllib.parse.urlencode({"name": parsed.hostname, "type": "A"})
with urllib.request.urlopen("https://dns.alidns.com/resolve?" + query, timeout=10) as response:
answer = json.loads(response.read(65536))
ips = [ipaddress.ip_address(record["data"]) for record in answer.get("Answer", [])
if record.get("type") in (1, 28)] if answer.get("Status") == 0 else []
if not ips:
raise ValueError("Public DNS lookup returned no artifact address")
for ip in ips:
if not ip.is_global:
raise ValueError("Remote artifact URL resolves to a non-public address")