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 getResult(); /** * Returns the original subnets that were merged during summarisation. */ Set getMergedSubnets(); } /** * Performs subnet merging strategies based on overlap and utilisation heuristics. */ private static class SubnetSummaryUtility implements SummarisationResult { @Getter private final List result; private final Collection source; @Getter private final Set mergedSubnets = new HashSet<>(); /** * Builds the summarisation utility with the supplied subnets and usage threshold. */ public SubnetSummaryUtility(Collection 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(); 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 findMergeCandidatesForPrefixLength(int prefixLength) { //создаём подсети-кандидаты, которые покроют наш список var maxAddress = findMaxAddress(); var mask = IPv4Util.calculateMask(prefixLength); var firstAddress = (findMinAddress() & mask); var lastAddress = (maxAddress & mask); var candidates = new ArrayList(); 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(); 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 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); } }