pf-sdn/src/main/java/ru/kirillius/pf/sdn/Util/IPv4Util.java

355 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.kirillius.pf.sdn.Util;
import lombok.Getter;
import lombok.SneakyThrows;
import ru.kirillius.pf.sdn.api.Networking.IPv4Subnet;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Helper methods for IPv4 address manipulation and subnet aggregation logic.
*/
public class IPv4Util {
private IPv4Util() {
}
private static final Pattern pattern = Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
/**
* Ensures the supplied string is a valid IPv4 address.
*/
public static void validateAddress(String address) {
if (!pattern.matcher(address).matches()) {
throw new IllegalArgumentException("Invalid IPv4 address: " + address);
}
}
/**
* Ensures the prefix length is within the allowed range 0..32.
*/
public static void validatePrefix(int prefix) {
if (prefix < 0 || prefix > 32) {
throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix);
}
}
/**
* Converts a dotted IPv4 address into its numeric representation.
*/
@SneakyThrows
public static long ipAddressToLong(String address) {
validateAddress(address);
var ip = InetAddress.getByName(address);
var bytes = ip.getAddress();
var result = 0L;
for (var b : bytes) {
result = (result << 8) | (b & 0xFF);
}
return result;
}
/**
* Returns a bit mask representing the provided prefix length.
*/
public static long calculateMask(int prefixLength) {
validatePrefix(prefixLength);
return 0xFFFFFFFFL << (32 - prefixLength);
}
/**
* Converts a numeric IPv4 address into dotted notation.
*/
public static String longToIpAddress(long ipLong) {
if (ipLong < 0 || ipLong > 0xFFFFFFFFL) {
throw new IllegalArgumentException("Address number should be in range 0 - 4294967295");
}
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
}
/**
* Formats a numeric mask into dotted notation.
*/
public static String maskToString(long maskLong) {
return String.format("%d.%d.%d.%d",
(maskLong >> 24) & 0xff,
(maskLong >> 16) & 0xff,
(maskLong >> 8) & 0xff,
maskLong & 0xff);
}
/**
* Result contract for subnet summarisation operations.
*/
public interface SummarisationResult {
/**
* Returns the resulting set of subnets after summarisation.
*/
List<IPv4Subnet> getResult();
/**
* Returns the original subnets that were merged during summarisation.
*/
Set<IPv4Subnet> getMergedSubnets();
}
/**
* Performs subnet merging strategies based on overlap and utilisation heuristics.
*/
private static class SubnetSummaryUtility implements SummarisationResult {
@Getter
private final List<IPv4Subnet> result;
private final Collection<IPv4Subnet> source;
@Getter
private final Set<IPv4Subnet> mergedSubnets = new HashSet<>();
/**
* Builds the summarisation utility with the supplied subnets and usage threshold.
*/
public SubnetSummaryUtility(Collection<IPv4Subnet> subnets, int usePercentage) {
source = subnets;
result = new ArrayList<>(subnets);
summaryOverlapped();
mergeNeighbours();
summaryWithUsage(usePercentage > 50 ? usePercentage : 51);
summaryOverlapped();
result.sort(Comparator.comparing(IPv4Subnet::getLongAddress));
}
/**
* Removes redundant subnets wholly covered by other subnets.
*/
private void summaryOverlapped() {
if (result.size() < 2) {
return;
}
//check subnets overlaps
var overlapped = new ArrayList<IPv4Subnet>();
var orderedByPrefix = result.stream().sorted(Comparator.comparing(IPv4Subnet::getPrefixLength)).toList();
orderedByPrefix.stream()
.filter(subnet -> subnet.getPrefixLength() > 32)
.forEach(parent -> orderedByPrefix.forEach(subnet -> {
if (subnet.equals(parent)) {
return;
}
if (parent.overlaps(subnet)) {
overlapped.add(subnet);
if (source.contains(subnet)) {
mergedSubnets.add(subnet);
}
}
}));
overlapped.forEach(result::remove);
}
/**
* Attempts to merge adjacent subnets into larger ones while preserving coverage.
*/
private void mergeNeighbours() {
if (result.size() < 2) {
return;
}
var availableLengths = result.stream().map(IPv4Subnet::getPrefixLength).collect(Collectors.toSet());
for (var length = 32; length > 0; length--) {
if (!availableLengths.contains(length)) {
continue;
}
var finalLength = length;
var largerPrefixLength = length - 1;
var largerPrefixMask = IPv4Util.calculateMask(largerPrefixLength);
var selectedSubnets = result.stream().filter(subnet -> subnet.getPrefixLength() == finalLength).sorted(Comparator.comparing(IPv4Subnet::getLongAddress)).toList();
//проверяем является ли адрес подсети таким же как адрес подсети с перфиксом -1
for (var i = 0; i < selectedSubnets.size() - 1; i++) {
var subnet = selectedSubnets.get(i);
var next = selectedSubnets.get(i + 1);
var firstAddress = subnet.getLongAddress();
if (firstAddress != (firstAddress & largerPrefixMask)) {
continue;
}
var largerSubnet = new IPv4Subnet(subnet.getAddress(), largerPrefixLength);
if (largerSubnet.overlaps(next)) {
//если подсеть перекрывает соседнюю, то удаляем обе и добавляем новую
availableLengths.add(largerPrefixLength);
result.remove(subnet);
result.remove(next);
result.add(largerSubnet);
if (source.contains(subnet)) {
mergedSubnets.add(subnet);
}
if (source.contains(next)) {
mergedSubnets.add(next);
}
i++;
}
}
}
}
/**
* Returns the smallest prefix length currently present in the result set.
*/
private int findMinPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
}
/**
* Returns the largest prefix length currently present in the result set.
*/
private int findMaxPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
}
/**
* Returns the smallest network address found in the result set.
*/
private long findMinAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
}
/**
* Returns the largest network address found in the result set.
*/
private long findMaxAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
}
/**
* Produces candidate subnets capable of covering the current set at the given prefix length.
*/
private List<IPv4Subnet> findMergeCandidatesForPrefixLength(int prefixLength) {
//создаём подсети-кандидаты, которые покроют наш список
var maxAddress = findMaxAddress();
var mask = IPv4Util.calculateMask(prefixLength);
var firstAddress = (findMinAddress() & mask);
var lastAddress = (maxAddress & mask);
var candidates = new ArrayList<IPv4Subnet>();
var candidateAddress = firstAddress;
do {
var candidate = new IPv4Subnet(candidateAddress, prefixLength);
candidates.add(candidate);
if (candidates.size() > result.size()) {
throw new IllegalStateException("Too many IPv4 addresses when trying to summary " + result.size() + " subnets");
}
//поиск следующего адреса кандидата
var nextAddress = candidateAddress + candidate.count();
var nextSubnet = result.stream().filter(subnet -> {
var address = subnet.getLongAddress();
return subnet.getPrefixLength() > prefixLength && address >= nextAddress && address <= maxAddress;
}).min(Comparator.comparingLong(IPv4Subnet::getLongAddress)).stream().findFirst();
if (nextSubnet.isEmpty()) {
break;
}
candidateAddress = IPv4Util.createSubnetOverlapping(nextSubnet.get().getLongAddress(), prefixLength).getLongAddress();
} while (candidateAddress <= lastAddress);
return candidates;
}
/**
* Tests whether the candidate subnet meets utilisation requirements and performs the merge.
*/
private boolean testCandidate(IPv4Subnet candidate, int usePercentage) {
if (result.contains(candidate)) {
return false;
}
var min = candidate.getLongAddress();
var max = candidate.getLongAddress() + candidate.count() - 1;
var overlapped = new ArrayList<IPv4Subnet>();
var used = new AtomicLong(0L);
result.forEach(child -> {
if (child.getLongAddress() < min || child.getLongAddress() > max) {
return;
}
if (candidate.overlaps(child)) {
overlapped.add(child);
used.addAndGet(child.count());
}
});
if (100.0 * used.get() / candidate.count() >= usePercentage) {
//подсеть подходит под критерий
overlapped.forEach(subnet -> {
if (source.contains(subnet)) {
mergedSubnets.add(subnet);
}
});
result.removeAll(overlapped);
result.add(candidate);
return true;
}
return false;
}
/**
* Iteratively merges subnets that satisfy the utilisation threshold.
*/
private void summaryWithUsage(int usePercentage) {
if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) {
return;
}
var found = new AtomicBoolean();
do {
found.set(false);
if (result.size() < 2) {
break;
}
var prefixMin = findMinPrefixLength();
var prefixMax = findMaxPrefixLength();
for (var testPrefixLength = prefixMin - 1; testPrefixLength < prefixMax; testPrefixLength++) {
//создаём подсети-кандидаты, которые покроют наш список
var candidates = findMergeCandidatesForPrefixLength(testPrefixLength);
candidates.forEach(candidate -> {
if (testCandidate(candidate, usePercentage)) {
found.set(true);
}
});
if (candidates.stream().anyMatch(result::contains)) {
//если был добавлен хотя бы 1 кандидат, то нужно пересчитать maxPrefix
prefixMax = result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
}
}
} while (found.get());
}
}
/**
* Summaries the provided subnets using merging heuristics and utilisation thresholds.
*/
public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) {
return new SubnetSummaryUtility(subnets, usePercentage);
}
/**
* Creates a subnet covering the given address at the specified prefix length.
*/
private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), prefixLength);
}
/**
* Returns the number of addresses represented by a prefix length.
*/
public static long calculateCountForPrefixLength(long prefixLength) {
return 1L << (32L - prefixLength);
}
}