Proxy Pool (Bright Data ISP 1:1) — Superadmin

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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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 (faltou WORKER_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#

text
available --> assigned --> (banido ou desatribuido)
                  ^                |
                  |                v
                  +-- cooldown --- (72h)
                  |
                  +-- decommissioned (removido para sempre)
  • available: livre, pode ser alocado na próxima criação de instância
  • assigned: em uso 1:1 com uma instância
  • banned: reservado para casos manuais
  • cooldown: 72h fora do pool; auto-retorna a available
  • decommissioned: 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 para cooldown quando 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 provedor
  • BRIGHT_DATA_HOST (default brd.superproxy.io), BRIGHT_DATA_PORT (default 33335)
  • BRIGHT_DATA_DEFAULT_CITY (default br-saopaulo)
  • BRIGHT_DATA_MIN_FREE_POOL (default 2) — auto top-up mantem pelo menos esses IPs livres
  • BRIGHT_DATA_MAX_POOL_SIZE (default 100) — teto duro de IPs para controlar custo
  • STEALTH_UTLS_ENABLED (default false) — ativa os tres HTTP clients de sessão com uTLS no instance:connect; quando false, o worker usa o caminho legado com SetProxyAddress
  • STEALTH_FAIL_CLOSED (default false) — recusa instance:connect e GET /v1/instances/{instanceId}/stealth/verify sem proxy dedicado em vez de cair para egress direto
  • STEALTH_BEHAVIOR_ENABLED (default false) — instala o scheduler central de restore/reconnect/post-pair/auto-presence e ativa os hooks ADR-009 no fork do whatsmeow; quando false, o worker preserva os sleeps e backoffs legados
  • STEALTH_DNS_HARDENED_ENABLED (default false) — troca o resolver dos validators SSRF (ValidateMediaURL e webhook URL validation) para DNS-over-TLS em 1.1.1.1:853 e faz o client default de upload por media_url usar o mesmo resolver em vez do OS resolver
  • STEALTH_IP_COHERENCE_ENABLED (default false) — rebind por DDD -> city no instance:connect; quando false, 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:

json
{
  "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.