Recentemente, obtivemos o status de LIR e um bloco IPv6 /29. Logo surgiu a necessidade de registrar as sub-redes designadas. Já que nossa cobrança foi desenvolvida em PHP, tivemos que nos familiarizar com a questão e perceber que essa linguagem não é a mais amigável em termos de trabalhar com IPv6. Abaixo, apresentamos nossa solução para os desafios surgidos em relação a endereços e intervalos. Pode não ser a abordagem mais elegante, mas cumpre as tarefas propostas.

Um pouco de teoria
Isenção de responsabilidade. Se você está familiarizado com o que é IPv6 e como se utiliza, esta parte pode ser um pouco monótona para você. Ou pode não ser.
As pessoas que veem uma anotação IPv6 pela primeira vez podem se sentir desorientadas. Após anotações elegantes, 64.233.177.101 de repente nos deparamos com 2607:f8b0:4002:c08::8b e podemos ficar confusos. Ambos representam de forma legível 32 e 128 bits, respectivamente. Qualquer pacote IP contém um cabeçalho com o destino de cada bit estritamente padronizado. Sem entrar mais a fundo na estrutura dos cabeçalhos, precisamos retirar uma conclusão: para operações com endereços IP e intervalos, é mais conveniente usar matemática binária e operações bit a bit. Armazená-los no banco de dados também é mais fácil como BINARY(4) para IPv4 e BINARY(16) para IPv6.
Outro aspecto importante que vale a pena abordar são as máscaras de rede e a notação CIDR. CIDR é um acrônimo para Classless Inter-Domain Routing (). Este conceito substituiu a abordagem por classes em determinar qual parte do endereço IP é o prefixo da rede e qual é o endereço da interface da rede dentro dessa rede. Na prática, os primeiros n bits correspondentes ao prefixo são definidos como 1, enquanto os outros são 0.
De forma compreensível, isso é escrito como ip.add.re.ss/cidr. Por exemplo, 64.233.177.0/24 indica que os primeiros 24 bits pertencem ao prefixo. Os últimos 8 bits, que também são o último número na anotação legível, pertencem ao endereço dentro da sub-rede. Mais alguns exercícios. 64.233.177.101/32 e 2607:f8b0:4002:c08::8b/128 é um endereço específico. 2607:f8b0:4002:c08::/64 indica que os primeiros 64 bits (as primeiras 4 grupos) são o prefixo, e os outros 64 bits representam a parte local. Aliás, se alguém fica confuso com "::" na anotação, os dois pontos duplos substituem qualquer número de seções contendo 0. Eles podem aparecer na anotação apenas uma vez. Em outras palavras, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.
O que precisamos extrair de tudo isso? Em primeiro lugar, o primeiro e o último endereço da sub-rede podem ser obtidos usando operações binárias AND e OR, conhecendo a máscara em forma binária. Em segundo lugar, a próxima sub-rede de determinado tamanho (ou seja, com CIDR) n pode ser calculada adicionando 1 à n-ésima posição na representação binária. Por representação binária, refiro-me ao resultado do uso das funções pack() e inet_pton() e ao uso subsequente de , sob binário — a representação no sistema binário que pode ser obtida, por exemplo, através de base_convert().
HistóricoAntes da classificada, havia a segregação de endereços sem classes. Naqueles tempos longínquos, ninguém imaginava que haveria tantas sub-redes, eram distribuídas à vontade em grandes blocos: classe A — o prefixo eram os primeiros 8 bits (ou seja, o primeiro número), com o bit líder 0; classe B — os primeiros 16 (os dois primeiros números), bits líderes 10; classe C — os primeiros 24 bits, bits líderes 110. Esses bits líderes definiram os intervalos em que um endereço da classe correspondente era disponibilizado: 0.0.0.0 — 127.255.255.255 para classe A, 128.0.0.0 — 191.255.255.255 — classe B, 192.0.0.0 — 223.255.255.255 — classe C. À medida que a internet se espalhava pelo planeta, os reguladores perceberam que cometeram um erro, e no início dos anos 90 desenvolveram a concepção sem classes, permitindo não se prender aos bits líderes. Um pouco mais de detalhes podem ser extraídos, por exemplo, em .
Vamos à prática
Na prática, implementaremos as três tarefas mais prováveis, na minha opinião:
- determinar o primeiro e o último endereço de um intervalo;
- determinar o próximo intervalo de tamanho definido (CIDR);
- verificar se um endereço pertence a um intervalo.
A implementação será para IPv6, mas se necessário, a lógica pode ser facilmente adaptada. Algumas ideias eu extrai , mas implementei de maneira um pouco diferente. Também nos exemplos não há verificação de erros de entrada. Então, vamos lá.
Como já mencionei, o primeiro e o último endereço de um intervalo podem ser determinados usando operações bit a bit, sabendo o início do intervalo e a máscara de sub-rede binária. Portanto, primeiro precisamos transformar o CIDR em uma máscara binária. Para isso, vamos reunir sua representação hex e embalá-la em binário.
function cidrToMask ($cidr) {
$mask = str_repeat('f', ceil($cidr / 4));
$mask .= dechex(4 * ($cidr % 4));
$mask = str_pad($mask, 32, '0');
return pack('H*', $mask);
}Chamada pack('H*', $mask) embala a representação hex de forma semelhante, como inet_pton(). Com a única diferença de que ao chamar pack() todos os 0 devem estar em seus lugares, e os dois pontos não devem estar na gravação, ao contrário do que acontece com uma gravação legível.
O próximo passo é calcular o início e o fim do intervalo. E aqui surgirão nuances. Operações bit a bit são limitadas pela arquitetura do processador. Consequentemente, no meu CubieTruck de 32 bits, que às vezes uso para várias testagens, processar todos os 128 bits de um endereço em uma única operação não será possível. No entanto, nada impede que dividamos em grupos de 32 bits (por via das dúvidas, quem sabe em quais processadores estaremos rodando).
function getRangeBoundary ($ip, $cidr, $which, $ipIsBin = false, $returnBin = false) {
$mask = cidrToMask($cidr);
if (!$ipIsBin) {
$ip = inet_pton($ip);
}
$ipParts = str_split($ip, 4);
$maskParts = str_split($mask, 4);
$rangeParts = [];
for ($i = 0; $i < count($ipParts); $i++) {
if ($which == 'start') {
/* Побитовый & адреса и маски оставит только биты префикса. */
$rangeParts[$i] = $ipParts[$i] & $maskParts[$i];
} else {
/* Побитовый | с обратной маской (~) оставит биты префикса и установит все биты локальной части в 1. */
$rangeParts[$i] = $ipParts[$i] | ~$maskParts[$i];
}
}
$rangeBoundary = implode($rangeParts);
if ($returnBin) {
return $rangeBoundary;
} else {
return inet_ntop($rangeBoundary);
}
}Para uso futuro, teremos a possibilidade de passar um IP e obter o resultado tanto em formato binário quanto em formato legível. O parâmetro $which aqui determina se queremos obter o início ou o fim do intervalo (valores 'start' ou 'end' respectivamente).
A próxima tarefa (que também é a mais prática para nossa empresa) é calcular o próximo intervalo. Para essa tarefa, não veio à mente nada melhor do que decompor o endereço em uma string binária e adicionar 1 na posição correta, depois juntar tudo de volta. Para que não apareçam artefatos, decidi durante a decomposição e montagem segmentar o endereço por byte.
function getNextBlock ($ipStart, $cidr, $ipIsBin = false, $returnBin = false) {
if (!$ipIsBin) {
$ipStart = inet_pton($ipStart);
}
$ipParts = str_split($ipStart, 1);
$ipBin = '';
foreach ($ipParts as $ipPart) {
$ipBin .= str_pad(base_convert(unpack('H*', $ipPart)[1], 16, 2), 8, '0', STR_PAD_LEFT);
}
/* Добавляем 1 в нужном разряде двоичного представления строки "влоб" :) */
$i = $cidr - 1;
while ($i >= 0) {
if ($ipBin[$i] == '0') {
$ipBin[$i] = '1';
break;
} else {
$ipBin[$i] = '0';
}
$i--;
}
$ipBinParts = str_split($ipBin, 8);
foreach ($ipBinParts as $key => $ipBinPart) {
$ipParts[$key] = pack('H*', str_pad(base_convert($ipBinPart, 2, 16), 2, '0', STR_PAD_LEFT));
}
$nextIp = implode($ipParts);
if ($returnBin) {
return $nextIp;
} else {
return inet_ntop($nextIp);
}
}No final, obteremos o prefixo do próximo intervalo de tamanho especificado em $cidr.Com esta função, alocamos blocos de endereços para nossos clientes.
Finalmente, a verificação se o endereço pertence ao intervalo. Por exemplo, alocamos um bloco /48 para distribuir aos clientes blocos /64, e precisamos garantir que, ao atribuir, não ultrapassamos os limites do bloco alocado (na prática, isso não acontecerá tão cedo, mas ainda há uma probabilidade). Aqui, tudo é simples. Obtemos o início e o fim do intervalo em formato binário e verificamos se o endereço está dentro dos limites.
function ipInRange ($ip, $rangeStart, $cidr) {
$start = getRangeBoundary($rangeStart, $cidr, 'start', false, true);
$end = getRangeBoundary($rangeStart, $cidr, 'end', false, true);
$ipBin = inet_pton($ip);
return ($ipBin >= $start && $ipBin <= $end);
}Espero que tenha sido útil. Quais outras funções relacionadas a endereços você acha que seriam úteis? Quaisquer complementos, comentários e revisões de código são muito bem-vindos nos comentários.
Se você já é nosso cliente ou está pensando em se tornar um, celebrando a publicação deste artigo, estamos oferecendo um bloco /64 completamente gratuito para todos os serviços de VPS ou servidor dedicado no data center Equinix Tier IV, na Holanda, mediante solicitação ao departamento de vendas, fornecendo o link deste artigo em seu ticket. A oferta é válida até março de 2020.
Um pouco de publicidade 🙂
Obrigado por permanecer conosco. Você gosta dos nossos artigos? Quer ver mais materiais interessantes? Apoie-nos fazendo um pedido ou recomendando para conhecidos. , uma alternativa única aos servidores entry-level, que criamos para você: (disponíveis opções com RAID1 e RAID10, até 24 núcleos e até 40GB DDR4).
Dell R730xd por 2 vezes mais barato no datacenter Equinix Tier IV em Amsterdã? Somente conosco na Holanda! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — a partir de $99! Leia sobre
Fonte: habr.com
