← Назад в базу знаний

Microsoft Azure через IPv6 прокси: парсинг документации и API

Microsoft Azure через IPv6 прокси: парсинг документации и API

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 мс.

✔️Купить прокси