# Normalização de endereços IPv6 para destinos de aprovação

Uma tela de aprovação que mostra uma string IPv6 bruta pede que uma pessoa faça o trabalho de um analisador sob pressão de tempo. Isso é um erro de design. O sistema precisa analisar a solicitação e convertê-la em um endpoint tipado, rejeitar ambiguidades, comparar o valor tipado e exibir uma forma estável que a pessoa reconheça na próxima solicitação.

A notação IPv6 oferece a invasores e a softwares comuns muitas maneiras de fazer o mesmo destino parecer diferente: campos zero comprimidos, zeros à esquerda, colchetes exigidos por URLs, uma terminação IPv4 incorporada e zonas de interface. Nada disso torna o IPv6 suspeito. Mas um destino de aprovação exige um tratamento mais rigoroso que um campo que por acaso contém um nome de host.

## Um endereço pode chegar em várias strings

As strings `2001:db8:0:0:0:0:0:9`, `2001:0db8::9` e `2001:db8::9` identificam o mesmo endereço IPv6 sem escopo. Se seu cartão de aprovação armazena uma string e uma solicitação posterior fornece outra, uma comparação de strings diz que elas são diferentes. Se sua lista de permissões aceita uma grafia, mas a busca de auditoria espera outra, as equipes perdem o rastro justamente quando precisam dele.

O IPv6 tem oito campos de 16 bits. Quem escreve pode omitir zeros à esquerda em cada campo e depois substituir uma sequência consecutiva de campos formados só por zeros por `::`. O marcador `::` é conveniente para humanos, mas elimina a informação sobre quantos campos foram omitidos. Um analisador restaura os campos ausentes e retorna a única representação que importa para decidir igualdade: 16 bytes de endereço.

Essa distinção costuma ser confundida: texto canônico serve para revisão, enquanto bytes analisados servem para comparação. A canonização, por si só, não é autorização. Ela apenas garante que a forma mostrada a uma pessoa não dependa da grafia usada por quem chamou.

Trate estes itens como dados separados:

- `raw_input`: o texto exato do host e a sintaxe de autoridade ao redor recebidos de quem fez a chamada
- `address`: 16 bytes após uma análise rigorosa
- `scope_id`: um escopo de interface quando a entrada o forneceu de forma válida
- `display_host`: uma string canônica gerada a partir do endereço tipado
- `port`, `scheme` e detalhes da solicitação: campos que descrevem a ação real

Mantenha a entrada bruta no registro de auditoria. Ela explica o que o agente pediu. Não a use como token de comparação e não deixe que seja a única informação na tela.

## Colchetes em URI são sintaxe, não parte do host

Um literal IPv6 dentro da autoridade de uma URI precisa estar entre colchetes porque os dois-pontos já separam um host de uma porta. O RFC 3986 define a forma: `https://[2001:db8::9]:8443/v1/jobs`. O endereço é `2001:db8::9`; os colchetes informam ao analisador de URI onde esse host termina.

Essa regra produz uma falha comum. Uma pessoa desenvolvedora passa `[2001:db8::9]` a um analisador de endereços, recebe uma rejeição, remove caracteres até funcionar e, mais tarde, aceita uma autoridade malformada porque os dois analisadores deixaram de concordar. A outra versão do mesmo erro armazena os colchetes junto ao endereço, de modo que um registro tem `[2001:db8::9]` e outro tem `2001:db8::9`. Eles nunca deveriam ter chegado ao mesmo campo de armazenamento.

Analise na fronteira gramatical em que o valor chegou. Para um destino HTTP, primeiro analise a URI completa com um analisador de URI compatível com os padrões. Extraia host, porta, esquema, caminho e consulta de acordo com esse analisador. Em seguida, envie ao analisador IPv6 apenas o valor do host, sem colchetes de URI. Para um argumento direto de host SSH, use a gramática documentada pela interface de comando SSH, em vez de fingir que ele é uma URI.

A ordem importa. Considere esta solicitação:

```text
https://[2001:0db8:0:0:0:0:0:9]:8443/admin
```

Um resultado interno correto tem este formato:

```text
kind: ipv6
address: 20010db8000000000000000000000009
scope_id: null
display_host: 2001:db8::9
scheme: https
port: 8443
path: /admin
```

Agora, uma interface de aprovação pode mostrar `https://[2001:db8::9]:8443/admin`. Ela não deve reduzir isso silenciosamente a `2001:db8::9`, pois a porta e o caminho fazem parte do que a pessoa avalia. Da mesma forma, não deve preservar a grafia preenchida com zeros só porque ela chegou primeiro.

Um literal isolado não tem colchetes. Uma autoridade URI com um literal IPv6 tem colchetes. Mantenha essa regra limitada e previsível.

## A exibição canônica deve seguir o RFC 5952

O RFC 5952 recomenda hexadecimal em minúsculas, sem zeros à esquerda em um campo e `::` para a maior sequência consecutiva de campos zero. Quando duas sequências de zeros têm o mesmo tamanho, ele escolhe a primeira. Também diz que um único campo zero não deve usar `::`. Essas regras dão à forma voltada a pessoas um resultado estável.

Por exemplo, normalize estes valores assim:

```text
2001:0DB8:0000:0000:0000:0000:0000:0009  ->  2001:db8::9
2001:db8:0:1:0:0:0:1                    ->  2001:db8:0:1::1
2001:db8:0:1:0:0:0:0                    ->  2001:db8:0:1::
2001:db8:0:1:0:0:0:2                    ->  2001:db8:0:1::2
0:0:0:0:0:0:0:1                         ->  ::1
```

A recomendação do padrão é mais útil do que parece. Ela dá às equipes uma única grafia para buscar nos logs e impede que alguém faça `2001:0DB8::9` parecer um destino novo depois que `2001:db8::9` já foi aprovado.

Não escreva seu próprio formatador dividindo por dois-pontos e contando strings vazias. A forma com terminação IPv4, a compressão dupla malformada e a sintaxe de escopo tornam essa abordagem frágil. Use um analisador IPv6 testado na linguagem que executa a ação, retenha sua saída de 16 bytes e formate a partir desses bytes. Teste o formatador com exemplos do RFC 5952 e casos com sequências de zeros de mesmo tamanho.

Também não atribua ao RFC 5952 mais do que ele diz. Ele descreve uma representação textual recomendada. Não decide se `::ffff:192.0.2.7` e `192.0.2.7` devem receber a mesma autorização. Essa é uma decisão de produto com consequências reais.

## Formas mapeadas para IPv4 precisam de uma regra explícita de comparação

`::ffff:192.0.2.7` é um endereço IPv6 mapeado para IPv4. Os 32 bits finais carregam um endereço IPv4, e o padrão anterior identifica a forma mapeada. Sistemas operacionais costumam expor essa forma quando uma aplicação aceita conexões IPv4 em um socket IPv6. Ela aparece em logs com frequência suficiente para que tratá-la como um caso extremo estranho garanta confusão mais tarde.

Há dois modelos internos defensáveis. Escolha um para cada fronteira de autorização e documente-o na interface.

O primeiro modelo preserva as famílias de endereços. `::ffff:192.0.2.7` continua sendo um endereço IPv6 de 16 bytes com tipo `ipv6`, enquanto `192.0.2.7` continua sendo um endereço IPv4 de quatro bytes com tipo `ipv4`. Eles nunca se comparam como iguais. Esse é o padrão mais seguro quando uma aprovação descreve uma conexão de rede solicitada em uma forma específica, pois evita ampliar silenciosamente uma decisão entre famílias.

O segundo modelo projeta endereços mapeados para IPv4 em um caso de uso de identidade de par estritamente definido. Nele, o analisador registra tanto a família original quanto um valor `embedded_ipv4`. O código de comparação afirma deliberadamente que uma forma mapeada equivale ao seu endereço IPv4 incorporado para essa única finalidade. O registro de auditoria ainda preserva a forma bruta e explica que a comparação usou uma projeção.

O que falha é a projeção acidental. Muitas bibliotecas padrão oferecem um método conveniente que converte um endereço mapeado em IPv4 e não informa como ele chegou. Isso é útil para registrar conexões, mas é perigoso se alguém reutilizá-lo para um token de aprovação. Uma regra escrita para `192.0.2.7` pode então aprovar `::ffff:192.0.2.7` sem que ninguém tenha decidido que deveria.

Use pares de teste que deixem a escolha visível:

```text
input A: 192.0.2.7
input B: ::ffff:192.0.2.7

strict endpoint comparison: different
explicit peer-identity projection: same IPv4 peer, if the product says so
```

Não trate toda string IPv6 com uma terminação pontuada como mapeada. O analisador precisa validar o prefixo completo e a posição da parte IPv4. O RFC 4291 define endereços IPv4 mapeados e também permite notação compatível com IPv4 no texto IPv6. Seu formatador deve preservar informação tipada suficiente para não chamar todos os endereços com terminação pontuada de mesma coisa.

## Identificadores de zona pertencem a uma interface local

Um identificador de zona transforma um endereço com escopo que seria ambíguo em um destino local utilizável. `fe80::1%en0` significa um endereço local de enlace na interface chamada `en0`. Sem a zona, um host com mais de uma interface de rede não consegue saber a qual enlace quem chamou se refere.

O RFC 4007 descreve isso como um conceito de zona de escopo, não como decoração acrescentada a um endereço. O nome ou índice da interface só tem significado para o host que o resolve. Em outra máquina, `en0` pode nomear outra interface ou nenhuma. Isso faz de um identificador de zona um alvo de aprovação portátil ruim.

Para uma ação direta em socket local, aceite uma zona apenas se o analisador da plataforma a validar e se a ação for executada na mesma máquina. Armazene o índice numérico normalizado da interface como valor de comparação quando o SO o expuser. Você também pode manter o nome de interface fornecido para a trilha de auditoria, mas nomes podem mudar quando hardware e configuração de rede mudam.

Para uma URI HTTP, as regras são mais rígidas. O RFC 6874 especifica que o sinal de porcentagem que introduz um identificador de zona é codificado como `%25` dentro do literal entre colchetes. Portanto, uma URI usa uma forma como esta:

```text
http://[fe80::1%25en0]/status
```

Um analisador de URI deve decodificar isso na etapa correta. Não decodifique percentuais na URL inteira antes de analisá-la, pois um decodificador genérico pode alterar delimitadores e criar uma URL diferente da fornecida. Primeiro analise a URI, extraia o literal do host e depois aplique as regras de literal com escopo exigidas para esse componente.

A maioria dos fluxos de aprovação HTTP voltados a agentes deve rejeitar literais com escopo e explicar o motivo: endereços locais de enlace só fazem sentido com uma interface local específica. Pedir que um agente use um nome DNS estável, um endereço sem escopo ou um endpoint local configurado deliberadamente cria uma aprovação que outra pessoa consegue entender. Permita a exceção apenas quando o produto realmente opera com equipamentos de rede locais e mostra claramente o escopo da interface.

## Comparar hosts é menor que aprovar uma ação

Normalizar um endereço corrige uma classe restrita de engano. Isso não torna `https://[2001:db8::9]/` equivalente a `https://[2001:db8::9]:9443/delete`, nem torna um destino SSH seguro porque o texto do host parece familiar.

O destino de aprovação tipado deve manter os limites que a string bruta esconde. Para HTTP, isso normalmente inclui pelo menos esquema, tipo e bytes do host, porta após resolver a porta padrão, método e uma descrição da solicitação que exponha o caminho. A inclusão de cabeçalhos e hash do corpo na decisão depende da ação. Se uma credencial bearer puder chamar várias APIs sem relação entre si, a identidade da credencial também deve aparecer no prompt.

Para SSH, diferencie um endpoint de rede do comando remoto. Uma aprovação para abrir uma conexão SSH não descreve automaticamente permissão para executar `sudo`, alterar arquivos de implantação ou encaminhar uma porta. Se uma ferramenta puder realizar várias operações após uma conexão, a interface deve informar o que a autorização da sessão abrange e manter um registro por chamada de cada comando.

É aqui que um modelo de dados bem definido compensa. Ele evita uma recomendação tentadora, mas errada: colocar hosts canônicos em uma lista de permissões simples e considerar o trabalho concluído. Listas simples de hosts são populares porque são fáceis de explicar. Elas falham porque um host é apenas uma parte de uma ação e porque uma resposta DNS posterior, porta, caminho ou comando pode mudar o efeito.

Um registro de aprovação útil pode ter esta aparência:

```text
transport: https
host_kind: ipv6
host_bytes: 20010db8000000000000000000000009
host_display: 2001:db8::9
scope_id: null
port: 8443
method: POST
path: /v1/releases
credential_ref: deploy-api
raw_authority: [2001:0db8::9]:8443
```

A autoridade bruta documenta a solicitação. Os campos tipados conduzem a comparação. O campo de exibição dá à pessoa uma frase estável para ler. Não reutilize um desses campos como substituto dos outros.

## Regras de rejeição devem ser simples e rigorosas

Um analisador deve falhar de forma segura quando a entrada não se encaixa na gramática de sua posição. A mensagem de erro pode ajudar, mas o sistema não deve corrigir um endereço tentando adivinhar o que quem chamou pretendia. Um normalizador que aceita entrada quase válida cria uma segunda gramática que profissionais de revisão de segurança terão dificuldade para reconstruir.

Rejeite uma solicitação de literal IP quando ela tiver qualquer uma destas falhas:

- mais de um marcador de compressão `::` ou campos demais após a expansão
- caracteres não hexadecimais em um campo hexadecimal
- uma terminação IPv4 fora da posição permitida pela sintaxe textual IPv6
- colchetes passados a um analisador de endereço isolado, ou ausência de colchetes em uma autoridade URI
- um identificador de zona em um contexto no qual a ação não pode vincular uma interface local

Também rejeite um literal que seu analisador de URI e seu analisador de socket interpretem de maneira diferente. Isso não é preciosismo teórico. Bibliotecas diferentes tomaram decisões diferentes ao longo do tempo sobre codificação de percentuais, formas incomuns de IPv4 e análise de hosts. Um componente aprovar um texto ao qual outro componente se conecta é uma lacuna de autorização.

Crie um pequeno conjunto de testes entre camadas. Passe cada caso pelo mesmo analisador de URI, extrator de host, analisador IP, formatador, comparador e construtor de transporte usados em produção. Para entradas aceitas, verifique tanto a exibição canônica quanto o destino real do socket. Para entradas rejeitadas, verifique que nenhum objeto de transporte seja criado.

Um conjunto compacto deve incluir `::`, `::1`, um endereço totalmente expandido, duas sequências de zeros de mesmo tamanho, um literal de URI entre colchetes com porta, um valor IPv4 mapeado, uma terminação pontuada malformada, um literal local de enlace com escopo e uma zona `%25` codificada em uma URI. Acrescente qualquer formato de entrada que agentes no seu ambiente realmente tenham produzido. Testes de regressão extraídos de aprovações reais são menos glamourosos que truques de análise e têm muito mais chance de detectar o próximo erro.

## O cartão de aprovação deve mostrar a normalização

Uma pessoa não consegue avaliar um endpoint se o cartão esconde a parte que mudou. Mostre o destino canônico em destaque e, quando for diferente, mostre a grafia enviada. Uma linha discreta como `Enviado como [2001:0DB8:0:0:0:0:0:9]:8443` dá evidência a quem revisa sem obrigá-lo a decodificar campos mentalmente.

O cartão também deve deixar claras as formas especiais. Identifique um endereço mapeado como `IPv6 mapeado para IPv4` em vez de apresentá-lo como um literal IPv6 comum. Identifique um endereço com escopo pelo nome da interface e informe que ele é local àquela interface. Se a política da ação projeta um endereço mapeado para IPv4, declare essa decisão no cartão. Equivalência silenciosa é o que surpreende quem revisa.

Para chamadas de alto impacto, exija que a pessoa aprove a ação completa, não apenas o host canônico. Uma apresentação compacta ainda pode trazer os detalhes necessários:

```text
POST https://[2001:db8::9]:8443/v1/releases
Uses credential: deploy-api
Submitted host spelling: 2001:0DB8:0:0:0:0:0:9
```

A Sallyport segue essa separação onde ela importa: o agente nunca recebe o segredo de API ou SSH, enquanto o app executa a ação e retorna seu resultado. Sua autorização por sessão pode estabelecer quem iniciou uma execução, e seus controles por chamada podem manter ações sensíveis visíveis, em vez de tratar uma aprovação anterior como autorização irrestrita.

## Registros de auditoria precisam do texto bruto e do significado normalizado

A revisão de incidentes precisa de duas respostas que frequentemente entram em conflito se você salvou apenas uma representação: o que o agente realmente enviou e qual destino o transporte usou? Salve ambos. Depois, deixe claro qual valor conduziu a decisão.

Um evento de auditoria deve incluir a entrada bruta, o tipo de host analisado, a forma canônica de exibição, bytes de endereço em uma codificação inequívoca, identidade de escopo quando houver e todos os campos de ação cobertos pela aprovação. Aplicar hash ao evento após a serialização só é útil se a serialização tiver uma definição estável. Caso contrário, o mesmo evento semântico pode produzir registros diferentes porque um formatador mudou.

Não apague entradas não canônicas depois de derivar a forma de exibição. Elas podem revelar um cliente com erro, uma tentativa de contornar controles ou uma particularidade inofensiva de biblioteca que depois explica um incidente. Mantenha-as como evidência, mas não deixe que painéis de busca as transformem em uma segunda identidade enganosa para o mesmo endpoint.

Os diários de Atividades e Sessões da Sallyport são projetados a partir de um único log de auditoria criptografado e encadeado por hash, e `sp audit verify` verifica essa cadeia offline sobre texto cifrado. Essa estrutura é especialmente útil quando quem revisa precisa distinguir uma grafia incomum de uma ação executada diferente.

Comece com um teste antes de redesenhar toda a camada de aprovação: envie o mesmo destino em formato totalmente expandido e no formato RFC 5952. Se o sistema produzir identidades de aprovação diferentes, resultados de busca de auditoria diferentes ou decisões de permissão diferentes, a fronteira do analisador ainda está no lugar errado.
