355 lines
14 KiB
Java
355 lines
14 KiB
Java
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);
|
||
}
|
||
|
||
|
||
}
|