17.X Proxy Pool (Bright Data ISP 1:1) — Superadmin#
Cada instância WhatsApp roda atrás de um IP residencial dedicado (Bright Data, zona isp_br) — 1 IP por instância, sem compartilhamento entre clientes. Endpoints abaixo expoem o pool; clientes normais não veem.
Quando STEALTH_IP_COHERENCE_ENABLED=true, o worker reavalia o proxy no instance:connect: deriva DDD -> city pelo catalogo compartilhado de fingerprint, reaproveita um IP available saudável (last_health_ok=true) na cidade correspondente ou provisiona 1 novo IP no Bright Data nessa cidade. DDD desconhecido ou telefone ausente cai para br-saopaulo. O script noturno scripts/proxy-pool-warm-cities.sh preaquece 5 IPs livres nas 10 maiores cidades BR (br-saopaulo, br-rio-de-janeiro, br-belo-horizonte, br-curitiba, br-porto-alegre, br-brasilia, br-salvador, br-recife, br-fortaleza, br-belem) sem ultrapassar BRIGHT_DATA_MAX_POOL_SIZE.
GET/v1/admin/proxy-pool#
Lista IPs do pool com totais por status e custo mensal.
Auth: Superadmin
Query: status (available|assigned|banned|cooldown|decommissioned), page, limit
Resposta 200:
{
"data": [
{
"id": 1,
"provider": "bright-data",
"zone": "isp_br",
"ip_address": "200.160.45.35",
"city": "br-saopaulo",
"isp": "",
"host": "brd.superproxy.io",
"port": 33335,
"status": "assigned",
"assigned_company_id": 3,
"assigned_instance": "9b1f8d20-312c-41fd-88d5-0de4648feefc",
"assigned_at": "2026-04-17T00:15:00Z",
"last_health_check": "2026-04-17T00:20:12Z",
"last_health_ok": true,
"ban_count": 0,
"cost_usd_per_month": 4,
"created_at": "2026-04-16T23:23:08Z",
"updated_at": "2026-04-16T23:23:19Z"
}
],
"total": 1,
"page": 1,
"limit": 50,
"counts_by_status": { "assigned": 1 },
"monthly_cost_usd": 4,
"zone": "isp_br",
"min_free_target": 2,
"max_pool_size": 50
}
Nota de seguranca: username e password NUNCA são retornados. Apenas host/porta públicos.
GET/v1/admin/proxy-pool/{proxyId}#
Detalhe de um único IP. Mesmo shape do item em data acima.
POST/v1/admin/proxy-pool/sync#
Reconcilia o pool local com a zone isp_br no Bright Data. Insere IPs novos, marca decommissioned os que sumiram no provedor, não mexe em assigned.
Resposta 200:
{
"inserted": ["200.160.45.35"],
"decommissioned": null,
"unchanged": 0,
"total": 1
}
POST/v1/admin/proxy-pool/provision#
Aloca N novos IPs dedicados no Bright Data ($4/IP/mes). Respeita BRIGHT_DATA_MAX_POOL_SIZE — se atingido, provisiona zero e retorna.
Body: { "count": 2 }
Resposta 200: { "requested": 2, "provisioned": 2 }
DELETE/v1/admin/proxy-pool/{proxyId}#
Remove o IP permanentemente (no provedor E no pool local). 409 se o IP está em assigned — libere a instância primeiro.
POST/v1/admin/proxy-pool/{proxyId}/health#
Roda um health check ao vivo: conecta via HTTP CONNECT ao ipinfo.io e confere que o exit IP bate com o esperado.
Resposta 200:
{
"id": 1,
"ok": true,
"ip_address": "200.160.45.35",
"last_health_check": "2026-04-17T00:20:12Z"
}
POST/v1/admin/proxy-pool/{proxyId}/wa-probe#
Roda um WhatsApp QR Probe sincrono através do IP. Abre uma sessão
disposable (whatsmeow + tmpdir SQLite + SetProxyAddress), espera o
primeiro QR code (sem parear), e fecha. Detecta IPs queimados antes
de qualquer cliente ver: ConnectFailure 401/402/403/406,
StreamError 401/403, TemporaryBan -> verdict=burned. Latencia
típica 10-15s.
Pre-requisito: WORKER_RPC_URL apontando para o worker e
WA_PROBE_AT_PROVISION em enabled ou manual_only no env. IP precisa
estar status=available (proibido tocar IPs já atribuidos a um cliente
real). Verdict + signal + latency são persistidos em proxy_ips.last_wa_probe_*
e emitem um row no proxy_ip_events (eternal log).
Resposta 200:
{
"verdict": "ok",
"signal": "qr_emitted",
"latency_ms": 9420,
"ip_address": "200.160.45.35",
"provider": "bright-data"
}
Verdicts:
ok— Meta aceitou ClientPayload e emitiu QR. IP fica no pool.burned— Meta rejeitou (ban-y signal). Operador pode descomissionar via DELETE.inconclusive— timeout / ClientOutdated / 5xx. IP fica como está; tentar de novo.
Erros:
- 503
WA_PROBE_DISABLED— RPC não wired (faltouWORKER_RPC_URL). - 503
PROXY_POOL_DISABLED— Bright Data não configurado. - 502
WA_PROBE_FAILED— RPC falhou (worker down, network).
Pre-release gate (provision + sync) chama o mesmo caminho via
SetValidateMetaReachabilityFunc no cmd/biazap-api/main.go —
quando WA_PROBE_AT_PROVISION=enabled, todo IP novo passa por essa
verificação antes de entrar como available. Burned verdicts no gate
disparam Decommission automático no provider; verdicts inconclusive
deixam o IP entrar com last_wa_probe_ok=NULL (re-probe manual depois).
POST/v1/admin/proxy-pool/{proxyId}/cooldown/release#
Forca um IP em cooldown de volta para available. Usado quando você está confiante de que o ban foi transiente.
Resposta 200: { "released": true, "id": 1 }
Ciclo de vida do IP#
available --> assigned --> (banido ou desatribuido)
^ |
| v
+-- cooldown --- (72h)
|
+-- decommissioned (removido para sempre)
available: livre, pode ser alocado na próxima criação de instânciaassigned: em uso 1:1 com uma instânciabanned: reservado para casos manuaiscooldown: 72h fora do pool; auto-retorna aavailabledecommissioned: removido no provedor, mantido no DB para auditoria
Nota operacional: o loop de health pong hoje persiste evidência (
proxy_health_events,last_health_*) e incrementa metricas, mas não move automaticamente o IP paracooldownquando detecta mismatch. O cooldown contínua sendo uma ação administrativa/manual.
Metricas Prometheus#
| Metrica | Labels | Descricao |
|---|---|---|
biazap_proxy_pool_total |
status |
Gauge: contagem por status |
biazap_proxy_pool_size |
city |
Gauge: tamanho atual do pool por cidade (exclui decommissioned) |
biazap_proxy_pool_cost_usd_monthly |
— | Gauge: custo mensal em USD (soma cost_usd_per_month de IPs não-decommissioned) |
biazap_proxy_alloc_total |
result (success/pool_exhausted/bd_failure) |
Counter de tentativas de alocação |
biazap_proxy_provision_total |
result (success/failure/capped/bad_ip/exhausted_retries) |
Counter de provisionamentos via BD API. bad_ip = IP provisionado mas rejeitado pela validação BR (discard no BD imediato). exhausted_retries = 5 tentativas consecutivas retornaram IPs não-BR, provisionamento abortado (operator precisa investigar). |
biazap_proxy_health_failures_total |
— | Counter de falhas em health check |
biazap_proxy_bans_total |
— | Counter de bans manuais (MarkBanned) |
biazap_fingerprint_city_total |
city |
Counter: cidade efetivamente entregue por AllocateForPhone / rebind coerente por DDD |
Env vars relacionadas#
BRIGHT_DATA_API_KEY,BRIGHT_DATA_CUSTOMER_ID,BRIGHT_DATA_ZONE,BRIGHT_DATA_ZONE_PASSWORD— credenciais provedorBRIGHT_DATA_HOST(defaultbrd.superproxy.io),BRIGHT_DATA_PORT(default 33335)BRIGHT_DATA_DEFAULT_CITY(defaultbr-saopaulo)BRIGHT_DATA_MIN_FREE_POOL(default 2) — auto top-up mantem pelo menos esses IPs livresBRIGHT_DATA_MAX_POOL_SIZE(default 100) — teto duro de IPs para controlar custoSTEALTH_UTLS_ENABLED(defaultfalse) — ativa os tres HTTP clients de sessão com uTLS noinstance:connect; quandofalse, o worker usa o caminho legado comSetProxyAddressSTEALTH_FAIL_CLOSED(defaultfalse) — recusainstance:connecteGET /v1/instances/{instanceId}/stealth/verifysem proxy dedicado em vez de cair para egress diretoSTEALTH_BEHAVIOR_ENABLED(defaultfalse) — instala o scheduler central de restore/reconnect/post-pair/auto-presence e ativa os hooks ADR-009 no fork do whatsmeow; quandofalse, o worker preserva os sleeps e backoffs legadosSTEALTH_DNS_HARDENED_ENABLED(defaultfalse) — troca o resolver dos validators SSRF (ValidateMediaURLe webhook URL validation) para DNS-over-TLS em1.1.1.1:853e faz o client default de upload pormedia_urlusar o mesmo resolver em vez do OS resolverSTEALTH_IP_COHERENCE_ENABLED(defaultfalse) — rebind porDDD -> citynoinstance:connect; quandofalse, o connect reutiliza a alocação legada
Resposta da instância com proxy#
Todas as respostas de instância (GET /v1/instances, GET /v1/instances/{id}) passam a incluir o campo proxy quando o pool está ativo:
{
"id": "9b1f8d20-312c-41fd-88d5-0de4648feefc",
"name": "ProxyTest1",
"status": "CONNECTED",
"proxy": {
"ip_address": "200.160.45.35",
"city": "br-saopaulo",
"isp": "",
"assigned_at": "2026-04-17T00:15:00Z"
},
"created_at": "..."
}
Erros: quando o pool está vazio e MaxPoolSize foi atingido, POST /v1/instances retorna 503 com error_code=PROXY_POOL_EXHAUSTED.