Microsoft Azure через IPv6 прокси: парсинг документации и API
Содержание
- Почему Azure так нервно реагирует на парсеров
- Что именно приходится парсить
- Настройка IPv6-прокси для curl
- Python-клиент с ротацией адресов
- Таблица: сравнение endpoint'ов по поведению
- Кейс: 429 на learn.microsoft.com при парсинге оглавления
- Кейс: TLS handshake failure на management.azure.com
- Работа с Bearer-токенами через прокси
- Кейс: Graph API и throttling на уровне приложения
- Документация: почему HTML мало
- Мониторинг и health-check прокси
- Что в итоге
Azure — это не один сайт, а зоопарк из сотен поддоменов: `learn.microsoft.com`, `docs.microsoft.com`, `management.azure.com`, `graph.microsoft.com`, `login.microsoftonline.com`. Каждый живёт своей жизнью, у каждого свои WAF-правила и свои капризы к источнику запроса. IPv6-прокси тут не роскошь, а способ развести потоки по разным адресам и не собрать бан за полчаса. Разбираем, как это работает на практике.
Почему Azure так нервно реагирует на парсеров
Front Door и Azure WAF стоят перед большинством публичных endpoint'ов. Правила стандартного OWASP-набора ловят аномалии по частоте запросов, по User-Agent, по геолокации IP. Плюс отдельный слой — rate limiting на уровне подписки и на уровне отдельного ресурса. Если с одного IPv4-адреса летит 200 запросов в минуту к `learn.microsoft.com`, вы довольно быстро получите 429 с заголовком `Retry-After`.
С IPv6 картина другая. У вас в /64 префиксе 2^64 адресов. Даже /48 даёт 65536 подсетей. Azure WAF считает rate limit по конкретному адресу, а не по префиксу. Значит, ротация адресов внутри /64 размазывает нагрузку так, что ни один счётчик не переполняется. Это не «обход защиты» — это нормальная работа с распределённой инфраструктурой, когда ваш легитимный краулер просто не должен выглядеть как DDoS с одной точки.
Есть нюанс. Некоторые endpoint'ы Azure (особенно `login.microsoftonline.com` и `management.azure.com`) требуют, чтобы IPv6-адрес был в глобальной таблице маршрутизации и не значился в известных списках datacenter-диапазонов. Прокси-пулы, которые светятся как «хостинг», отсекаются на этапе TLS-хендшейка — Azure просто рвёт соединение без объяснений.
Что именно приходится парсить
Документация Microsoft Learn — это SPA на React поверх статики. HTML отдаётся сервером, но оглавление и метаданные подгружаются через XHR к `learn.microsoft.com/api/...`. Если парсить только HTML, потеряете структуру навигации и версии статей. Если дёргать API без прокси — упрётесь в лимит 100 запросов в минуту на IP.
REST API Azure (`management.azure.com`) требует Bearer-токен из Entra ID. Токен привязан к tenant и client_id, но не к IP. Значит, можно ротировать адреса между запросами без перевыпуска токена — это ключевой момент для параллельного сбора данных по десяткам подписок.
Graph API (`graph.microsoft.com`) жёстче. Тут есть throttling на уровне приложения: 10000 запросов за 10 минут на app registration. IP-ротация не поможет, если вы долбите одним app_id. Зато помогает, когда у вас несколько service principal'ов и вы раскидываете их по разным IPv6-адресам, чтобы не словить burst detection.
Настройка IPv6-прокси для curl
Простейший сценарий — HTTP/HTTPS прокси с IPv6-адресом на выходе. curl с версии 7.54 умеет работать с IPv6-прокси через `--proxy`. Важно: не путайте `--proxy` (адрес прокси-сервера) и `--interface` (локальный интерфейс). Нам нужен первый.
```bash
curl -6 -x "http://[2a01:4f8:1c1c:abcd::1]:3128" \
-H "User-Agent: Mozilla/5.0 (compatible; DocBot/1.0)" \
-H "Accept: application/json" \
--max-time 15 \
--retry 3 --retry-delay 2 \
"https://learn.microsoft.com/api/search?search=virtual%20network&locale=en-us"
```
Флаг `-6` заставляет curl резолвить DNS только в AAAA. Это отсекает случайные IPv4-запросы, если прокси сконфигурирован как dual-stack и может «свалиться» на v4. Для чистоты эксперимента всегда проверяйте, каким адресом вас видит Azure:
```bash
curl -x "http://[2a01:4f8:1c1c:abcd::1]:3128" https://ifconfig.co/json
```
Ответ покажет `ip`, `ip_decimal` и `country`. Если там IPv4 — прокси не настроен на IPv6-выход, и весь смысл теряется.
Python-клиент с ротацией адресов
Для серьёзного парсинга `requests` уже не хватает — нужен пул прокси с health-check и автоматическим исключением мёртвых узлов. `httpx` умеет асинхронность и работает с HTTP/2, что для Azure полезно: многие endpoint'ы отдают разные заголовки при HTTP/1.1 и HTTP/2.
```python
import asyncio
import httpx
from itertools import cycle
PROXIES = [
"http://[2a01:4f8:1c1c:abcd::1]:3128",
"http://[2a01:4f8:1c1c:abcd::2]:3128",
"http://[2a01:4f8:1c1c:abcd::3]:3128",
]
proxy_pool = cycle(PROXIES)
async def fetch(client: httpx.AsyncClient, url: str) -> dict:
proxy = next(proxy_pool)
try:
r = await client.get(url, proxy=proxy, timeout=15.0)
r.raise_for_status()
return {"url": url, "status": r.status_code, "bytes": len(r.content)}
except httpx.HTTPStatusError as e:
return {"url": url, "status": e.response.status_code, "proxy": proxy}
except httpx.TransportError:
return {"url": url, "status": None, "proxy": proxy, "error": "transport"}
async def main():
limits = httpx.Limits(max_connections=20, max_keepalive_connections=10)
async with httpx.AsyncClient(http2=True, limits=limits) as client:
urls = [f"https://learn.microsoft.com/api/search?search=item{i}" for i in range(50)]
results = await asyncio.gather(*(fetch(client, u) for u in urls))
fails = [r for r in results if r["status"] != 200]
print(f"ok={len(results)-len(fails)} fail={len(fails)}")
asyncio.run(main())
```
Обратите внимание на `max_keepalive_connections=10`. Azure Front Door любит рвать idle-соединения через 60 секунд. Если держать слишком много keep-alive сокетов, половина из них будет мёртвой к моменту следующего запроса, и вы получите `RemoteProtocolError`. Десять — рабочий компромисс.
Таблица: сравнение endpoint'ов по поведению
| Endpoint | Rate limit (на IP) | Требует токен | IPv6-friendly | HTTP/2 |
|---|---|---|---|---|
| learn.microsoft.com | ~100 req/min | нет | да | да |
| management.azure.com | 12000 req/hour | да (Bearer) | да | да |
| graph.microsoft.com | 10000 req/10min (на app) | да (Bearer) | да | да |
| login.microsoftonline.com | 20 req/min | нет (OAuth flow) | условно | нет |
Цифры взяты из официальной документации Microsoft по throttling (раздел «Service-specific limits»). Они меняются — проверяйте актуальные перед запуском большого краулера. У `login.microsoftonline.com` IPv6-friendly стоит «условно»: часть прокси-пулов отсекается по ASN, даже если адрес валидный.
Кейс: 429 на learn.microsoft.com при парсинге оглавления
Задача — собрать полное дерево документации Azure за неделю. Скрипт на Python дёргал `/api/toc` для каждого раздела. Через 12 минут работы — стабильные 429 с `Retry-After: 30`. Прокси был один, IPv4, из европейского датацентра.
Причина: Front Door видел один IP и включал adaptive throttling после 1500 запросов за 10 минут. Причём `Retry-After` врал — реальный cool-down был около 4 минут, а не 30 секунд.
Решение: пул из 16 IPv6-адресов в одном /64, ротация round-robin, задержка 50-100 мс между запросами (jitter). Нагрузка та же — 1500 запросов за 10 минут, но размазана по 16 адресам, ~94 запроса на каждый. Ни одного 429 за трое суток. Время сбора дерева — 4 дня вместо ожидаемых 7.
Кейс: TLS handshake failure на management.azure.com
Скрипт получал токен через `client_credentials`, потом шёл в `management.azure.com/subscriptions?api-version=2022-12-01`. Первые 200 запросов проходили, потом — `SSLError: EOF occurred in violation of protocol`. Без 429, без тела ответа.
Причина оказалась в SNI и TLS-фингерпринте. Azure отсекает соединения, где TLS ClientHello не соответствует ожидаемому профилю для данного ASN. Прокси из «дешёвых» пулов с нестандартным TLS-стеком (старый OpenSSL, кастомный cipher suite) отваливался на 200-м запросе — видимо, срабатывал какой-то эвристический триггер.
Решение: перешли на прокси с OpenSSL 3.x и стандартным набором cipher suite. Плюс явно указали TLS 1.3 в httpx через `ssl.create_default_context()` с `minimum_version=TLSVersion.TLSv1_3`. Ошибки прекратились.
Работа с Bearer-токенами через прокси
Токен Entra ID живёт 60-90 минут. Перевыпуск — это отдельный запрос к `login.microsoftonline.com`, и вот тут IPv6-прокси может подвести: часть пулов Microsoft блокирует на этапе OAuth flow, если IP «подозрительный». Практика — держать отдельный, «чистый» прокси-адрес только для token endpoint, а тяжёлые запросы к API гонять через ротацию.
```bash
TOKEN=\$(curl -s -x "http://[2a01:4f8:1c1c:ffff::1]:3128" \
-X POST "https://login.microsoftonline.com/\${TENANT_ID}/oauth2/v2.0/token" \
-d "client_id=\${CLIENT_ID}" \
-d "client_secret=\${CLIENT_SECRET}" \
-d "scope=https://management.azure.com/.default" \
-d "grant_type=client_credentials" | jq -r .access_token)
curl -s -x "http://[2a01:4f8:1c1c:abcd::5]:3128" \
-H "Authorization: Bearer \${TOKEN}" \
"https://management.azure.com/subscriptions?api-version=2022-12-01"
```
Токен не привязан к IP, поэтому такой split работает. Единственное — не пытайтесь перевыпускать токен с того же адреса, с которого идёт основной поток запросов: можно словить корреляцию и временную блокировку app registration.
Кейс: Graph API и throttling на уровне приложения
Сбор данных по пользователям через `graph.microsoft.com/v1.0/users`. Ротация IPv6 работала, но на 8000-м запросе за 10 минут прилетел 429 с заголовком `x-ms-throttling-version: 2`. IP-ротация не помогала — лимит считался по `app_id`.
Причина: Graph throttling двухуровневый. Первый уровень — per-IP (обходится ротацией). Второй — per-app (не обходится никак, кроме нескольких service principal'ов). Документация Microsoft об этом упоминает вскользь, в разделе «Microsoft Graph throttling guidance».
Решение: разбили сбор на 4 service principal'а с разными `client_id`, каждый со своим пулом IPv6-адресов. Нагрузка распределилась, 429 исчезли. Дополнительно включили exponential backoff с базовой задержкой 500 мс и множителем 2 — на случай если всё же упрёмся.
Документация: почему HTML мало
`learn.microsoft.com` отдаёт разметку с `data-bi-name` атрибутами и микроразметкой Schema.org. Но версии статей (ms.date, ms.topic) лежат в метаданных, которые подгружаются отдельным XHR к `/api/metadata/...`. Если парсить только HTML — потеряете дату последнего обновления и не сможете отслеживать изменения.
Хорошая практика — комбинировать. HTML даёт контент, API даёт метаданные и структуру. Прокси-пул нужен для обоих потоков, но с разными лимитами: HTML-запросы тяжелее (200-800 КБ на страницу), API-ответы легче (5-50 КБ), значит и частота у них разная. Для HTML — 60 запросов в минуту на адрес, для API — 200.
Мониторинг и health-check прокси
Половина проблем с парсингом Azure — это мёртвые прокси, о которых скрипт не знает. Простой health-check раз в 5 минут экономит часы отладки. Проверяйте не только доступность прокси, но и то, каким адресом вас видит Azure.
```python
import httpx, time
def check_proxy(proxy: str) -> dict:
start = time.monotonic()
try:
r = httpx.get("https://ifconfig.co/json", proxy=proxy, timeout=8.0)
latency = (time.monotonic() - start) * 1000
data = r.json()
return {
"proxy": proxy,
"ip": data.get("ip"),
"v6": ":" in data.get("ip", ""),
"latency_ms": round(latency),
"ok": ":" in data.get("ip", ""),
}
except Exception as e:
return {"proxy": proxy, "ok": False, "error": type(e).__name__}
```
Если `v6` = False — прокси ушёл на IPv4-выход, и весь смысл ротации теряется. Такое бывает, когда провайдер прокси переключает маршрут при деградации IPv6-аплинка. Ловить это надо автоматически, а не руками.
Что в итоге
Azure терпим к IPv6-источникам, но не терпим к однообразию. Один адрес — бан через 10-15 минут. Шестнадцать адресов в /64 с jitter — работают сутками. Graph API — отдельная история, там IP-ротация не спасает от per-app лимитов. Token endpoint лучше держать на «чистом» адресе, а не гонять через общий пул. И всегда проверяйте, что прокси реально отдаёт IPv6-адрес — провайдеры любят молча переключаться на v4 при проблемах с аплинком. Для сбора документации в больших объёмах разумно держать пул из 8-16 адресов с автоматическим health-check и ротацией по round-robin с рандомной задержкой 50-200 мс.