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-providers#

Lista os provedores de proxy registrados com status enabled. Usado pela UI admin para popular o seletor de provedor no formulario de provisionamento. Resposta sanitizada — nunca retorna API keys, base URLs ou outra configuração sensível.

Auth: Superadmin

Resposta 200:

json
{
  "data": [
    { "name": "bright-data", "display_name": "Bright Data", "enabled": true },
    { "name": "proxy-seller", "display_name": "Proxy-Seller", "enabled": false }
  ],
  "total": 2
}

Erros: 503 PROXY_POOL_DISABLED — pool não configurado.

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,
      "last_health_error": "",
      "ban_count": 0,
      "ban_reason": "",
      "cooldown_until": null,
      "cost_usd_per_month": 4,
      "expires_at": null,
      "auto_renew": false,
      "provider_ip_id": "",
      "health_score": 100,
      "health_score_updated_at": "2026-04-29T00:00:00Z",
      "last_event_at": "2026-04-29T00:00:00Z",
      "total_allocations": 1,
      "total_pairs": 1,
      "total_qr_cycles": 1,
      "total_qr_timeouts": 0,
      "total_connect_failures": 0,
      "total_cooldowns": 0,
      "last_wa_probe_at": "2026-05-04T00:00:00Z",
      "last_wa_probe_ok": true,
      "last_wa_probe_signal": "qr_emitted",
      "last_wa_probe_latency_ms": 9420,
      "reputation_score": 100,
      "reputation_status": "ok",
      "reputation_checked_at": "2026-05-04T00:00:00Z",
      "reputation_provider_count": 2,
      "reputation_summary": "",
      "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 },
  "counts_by_provider": { "bright-data": { "assigned": 1 } },
  "monthly_cost_usd": 4,
  "monthly_cost_by_provider": { "bright-data": 4 },
  "providers": [
    { "name": "bright-data", "display_name": "Bright Data", "enabled": true }
  ],
  "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.

Erro comum a todos os endpoints de 17.X: 503 PROXY_POOL_DISABLED quando BRIGHT_DATA_* não está configurado.

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
}

Erro: 502 BAD_GATEWAY quando a sincronização com o provedor falha.

POST/v1/admin/proxy-pool/provision#

Aloca N novos IPs dedicados no provedor escolhido. bright-data (~$4/IP/mes) roteia pela EnsureCapacity (alocação city-aware, teto BRIGHT_DATA_MAX_POOL_SIZE, probe BR pos-provisionamento). Outros provedores roteiam pela ProvisionForProvider.

Body:

json
{ "provider": "bright-data", "count": 2, "country": "BRA" }
  • provider (opcional, default bright-data) — id do provedor registrado.
  • count (obrigatório) — deve ser >= 1.
  • country (opcional) — código de país para provedores multi-país.

Resposta 200: { "requested": 2, "provisioned": 2, "provider": "bright-data" }

Erros:

  • 400 BAD_REQUESTcount ausente ou < 1
  • 400 PROVIDER_NOT_REGISTERED — provider informado não está registrado/habilitado
  • 502 BAD_GATEWAY — provisionamento no provedor falhou

DELETE/v1/admin/proxy-pool/{proxyId}#

Remove o IP permanentemente (no provedor E no pool local). Resposta 200: { "decommissioned": true, "id": 1 }. Erros: 409 CONFLICT (IP em assigned — libere a instância primeiro), 404 NOT_FOUND, 400 BAD_REQUEST (id inválido), 503 PROXY_POOL_DISABLED.

POST/v1/admin/proxy-pool/{proxyId}/replace#

Troca o IP de uma linha SEM encolher o pool — a ação recomendada para "remover este IP ruim". bright-data: descomissiona a linha + provisiona um IP BR novo (cobranca proporcional). proxy-seller: submete /proxy/replace (gratis dentro do periodo pago). Retorna a linha (possivelmente nova) após a substituição.

Auth: Superadmin

Resposta 200:

json
{ "replaced": true, "id": 7, "row": { "...": "proxyIPDTO da nova linha" } }

Erros:

  • 400 BAD_REQUESTproxyId inválido
  • 404 NOT_FOUND — IP não existe
  • 429 PROXY_REPLACE_COOLDOWN — provedor está rate-limitando o replace
  • 409 CONFLICT — outra falha (ex.: IP em estado inválido)
  • 503 PROXY_POOL_DISABLED

PATCH/v1/admin/proxy-pool/{proxyId}/auto-renew#

Liga/desliga o flag auto_renew de uma linha. Aplica-se a linhas Proxy-Seller (pre-pagas); Bright Data tem cobranca continua e auto-renovação e N/A.

Auth: Superadmin

Body: { "enabled": true }

Resposta 200: { "id": 7, "auto_renew": true }

Erros: 400 BAD_REQUEST (id inválido ou JSON malformado), 404 NOT_FOUND, 503 PROXY_POOL_DISABLED.

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"
}

Erros: 503 PROXY_POOL_DISABLED, 400 BAD_REQUEST (id inválido). O body 200 também inclui last_health_error e, em caso de falha, error.

GET/v1/admin/proxy-pool/{proxyId}/health#

Snapshot de saúde atual do IP — score + histograma por tipo de evento + timestamps de atividade. Renderiza o card "saúde do IP" ao lado da gaveta de timeline. (Distinto do POST /v1/admin/proxy-pool/{proxyId}/health, que dispara um health check ao vivo.)

Auth: Superadmin

Resposta 200:

json
{
  "id": 7,
  "ip_address": "200.160.45.35",
  "status": "assigned",
  "city": "br-saopaulo",
  "provider": "bright-data",
  "health_score": 100,
  "health_score_updated_at": "2026-04-29T12:00:00.000Z",
  "last_event_at": "2026-04-29T12:00:00.000Z",
  "totals": {
    "allocations": 1, "pairs": 1, "qr_cycles": 1,
    "qr_timeouts": 0, "connect_failures": 0, "cooldowns": 0, "bans": 0
  },
  "assigned_instance": "9b1f8d20-...",
  "events_by_type": { "wa_probe_ok": 1 }
}

Erros: 503 PROXY_POOL_DISABLED, 400 INVALID_ID, 404 PROXY_NOT_FOUND.

POST/v1/admin/proxy-pool/{proxyId}/pong#

Dispara uma sonda pong sob demanda através do proxy do IP até /v1/internal/proxy-pong e persiste a evidência em proxy_health_events. Usa exatamente o mesmo código do loop agendado de 5 minutos — um resultado verde aqui e a mesma prova que o loop agendado produziria.

Auth: Superadmin

Resposta 200:

json
{
  "id": 42,
  "proxy_ip_id": 7,
  "ip_address": "200.160.45.35",
  "trigger": "manual",
  "ok": true,
  "match": true,
  "expected_ip": "200.160.45.35",
  "observed_ip": "200.160.45.35",
  "rtt_ms": 312,
  "x_forwarded_for": "200.160.45.35",
  "user_agent": "biazap-proxy-pong/1",
  "received_at": "2026-04-18T11:55:00Z",
  "server_time": "2026-04-18T11:55:00Z",
  "created_at": "2026-04-18T11:55:00Z"
}

Erros:

  • 503 PROXY_POOL_DISABLED — pool não configurado
  • 503 PROXY_HEALTH_DISABLEDPROXY_HEALTH_SECRET ou PROXY_HEALTH_PUBLIC_URL ausente
  • 400 BAD_REQUESTproxyId inválido
  • 404 NOT_FOUND — IP não existe
  • 502 BAD_GATEWAY — a sonda falhou

GET/v1/admin/proxy-pool/{proxyId}/pong-history#

Retorna os últimos N eventos pong de um IP, mais recentes primeiro. Usado pela gaveta de histórico de saúde na UI admin.

Auth: Superadmin Query: limit (default 100, max 500)

Resposta 200: { "data": [ { ...proxyHealthEventDTO... } ], "total": N, "limit": 100 }

Erros: 503 PROXY_POOL_DISABLED, 400 BAD_REQUEST (id inválido), 500 INTERNAL.

GET/v1/admin/proxy-pool/{proxyId}/events#

Log de eventos eterno de um IP do pool, mais recentes primeiro. Usado pela gaveta de timeline na UI admin. Eventos permanecem mesmo após o IP ser descomissionado.

Auth: Superadmin Query: limit (default 200, max 1000)

Resposta 200:

json
{
  "events": [
    {
      "id": 901,
      "proxy_ip_id": 7,
      "ip_address": "200.160.45.35",
      "event_type": "wa_probe_ok",
      "severity": "info",
      "company_id": 3,
      "instance_id": "9b1f8d20-...",
      "message": "WA probe verdict=ok",
      "health_delta": 5,
      "data": {},
      "occurred_at": "2026-04-29T12:00:00.000Z"
    }
  ],
  "total": 1,
  "by_type": { "wa_probe_ok": 1 }
}

Quando o recorder está desabilitado responde { "events": [], "total": 0, "by_type": {}, "recorder": "disabled", "proxy_id": 7, "event_kind": "proxy" }.

Erros: 503 PROXY_POOL_DISABLED, 400 INVALID_ID, 500 INTERNAL.

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).
  • 400 BAD_REQUESTproxyId inválido.

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 }

Erros: 503 PROXY_POOL_DISABLED, 400 BAD_REQUEST (id inválido), 409 CONFLICT (IP não está em cooldown).

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 continua 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
  • PROXY_FAIL_CLOSED (default true) — recusa instance:connect/verify quando a instância não tem binding de proxy dedicado, em vez de cair para egress direto (preserva o invariante 1:1 proxy/VPN)
  • DNS_HARDENED (default true) — resolve as URLs de mídia/webhook via DNS-over-TLS (1.1.1.1:853) em vez do resolver do SO; defesa de SSRF apenas (cobre ValidateMediaURL + validação de URL de webhook, NÃO o tráfego de sessão whatsmeow)
  • STEALTH_UTLS_NEW_INSTANCES_PROFILE (default vazio) — perfil uTLS sticky aplicado na criação da instância e imutavel pelo resto da vida dela; vazio aciona o seletor do pool quando STEALTH_UTLS_POOL_ENABLED=true
  • STEALTH_UTLS_POOL_ENABLED (default false) — habilita a seleção ponderada de perfil uTLS a partir do pool fingerprint_utls_profiles para instâncias novas
  • STEALTH_UTLS_BURN_CHECK_ENABLED (default false) — habilita o detector horario (dark-mode) de burn de perfis uTLS

As flags de rollout STEALTH_PROFILE_ENABLED, STEALTH_UTLS_ENABLED, STEALTH_FAIL_CLOSED, STEALTH_BEHAVIOR_ENABLED, STEALTH_HYGIENE_AUTO_ENABLED, STEALTH_IP_COHERENCE_ENABLED e STEALTH_DNS_HARDENED_ENABLED foram aposentadas em 2026-04-22 — substituidas por PROXY_FAIL_CLOSED + DNS_HARDENED (ambas ON por padrão).

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.