промежуточный коммит

This commit is contained in:
kirillius 2025-09-25 23:03:06 +03:00
parent 23201db5ce
commit 32e65a7742
20 changed files with 1084 additions and 34 deletions

View File

@ -32,6 +32,14 @@
<artifactId>org.eclipse.jgit</artifactId> <artifactId>org.eclipse.jgit</artifactId>
<version>7.3.0.202506031305-r</version> <version>7.3.0.202506031305-r</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.hierynomus/sshj -->
<dependency>
<groupId>com.hierynomus</groupId>
<artifactId>sshj</artifactId>
<version>0.39.0</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,24 +1,26 @@
package ru.kirillius.pf.sdn; package ru.kirillius.pf.sdn;
import ru.kirillius.pf.sdn.External.API.GitSubscription; import lombok.SneakyThrows;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
public class App extends AppContext { public class App extends AppContext {
private final static File configFile = new File("config.json"); private final static File configFile = new File("config.json");
@SneakyThrows
public App(File configFile) { public App(File configFile) {
super(configFile); super(configFile);
GitSubscription subscription = new GitSubscription(this);
RepositoryConfig repositoryConfig = new RepositoryConfig("test", GitSubscription.class, "https://git.kirillius.ru/kirillius/docker-decompose.git");
Map<String, NetworkResourceBundle> resources = subscription.getResources(repositoryConfig); getSubscriptionManager().triggerUpdate();
} }
static { static {
@ -28,7 +30,15 @@ public class App extends AppContext {
public static void main(String[] args) { public static void main(String[] args) {
try (App app = new App(configFile)) { try (App app = new App(configFile)) {
app.getEventsHandler().getNetworkManagerUpdateEvent().add(new EventListener<NetworkResourceBundle>() {
@Override
public void invoke(NetworkResourceBundle bundle) throws Exception {
SystemLogger.message("Network resource bundle updated.", CTX);
}
});
while (true) {
Thread.yield();
}
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@ -9,13 +9,20 @@ import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.ContextEventsHandler; import ru.kirillius.pf.sdn.core.ContextEventsHandler;
import ru.kirillius.pf.sdn.core.Networking.ASInfoService; import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
import ru.kirillius.pf.sdn.core.Networking.NetworkManager; import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
import ru.kirillius.pf.sdn.core.Plugin;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class AppContext implements Context, Closeable { public class AppContext implements Context, Closeable {
protected final static String CTX = AppContext.class.getSimpleName();
public AppContext(File configFile) { public AppContext(File configFile) {
try { try {
config = Config.load(configFile); config = Config.load(configFile);
@ -34,6 +41,30 @@ public class AppContext implements Context, Closeable {
ASInfoService.setProvider(new HEInfoProvider(this)); ASInfoService.setProvider(new HEInfoProvider(this));
networkManager = new NetworkManager(this); networkManager = new NetworkManager(this);
networkManager.getInputResources().add(config.getCustomResources()); networkManager.getInputResources().add(config.getCustomResources());
subscriptionManager = new SubscriptionManager(this);
subscribe();
initPlugins();
}
private void initPlugins() {
config.getEnabledPlugins().forEach(pluginClass -> {
var plugin = Plugin.loadPlugin(pluginClass, this);
loadedPlugins.add(plugin);
});
}
private final List<Plugin<?>> loadedPlugins = new ArrayList<>();
private void subscribe() {
var eventsHandler = getEventsHandler();
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
var manager = getNetworkManager();
var inputResources = getNetworkManager().getInputResources();
inputResources.clear();
inputResources.add(config.getCustomResources());
inputResources.add(bundle);
manager.triggerUpdate();
});
} }
@ -47,9 +78,18 @@ public class AppContext implements Context, Closeable {
private final Server server; private final Server server;
@Getter @Getter
private final ASInfoService ASInfoService; private final ASInfoService ASInfoService;
@Getter
private final SubscriptionManager subscriptionManager;
@Override @Override
public void close() throws IOException { public void close() throws IOException {
loadedPlugins.forEach(plugin -> {
try {
plugin.close();
} catch (IOException e) {
SystemLogger.error("Error closing plugin", CTX, e);
}
});
ASInfoService.close(); ASInfoService.close();
networkManager.close(); networkManager.close();
try { try {

View File

@ -0,0 +1,174 @@
package ru.kirillius.pf.sdn.External.API;
import lombok.*;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.json.JSONArrayProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.pf.sdn.core.AbstractPlugin;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public final class FRRPlugin extends AbstractPlugin<FRRPlugin.FRRConfig> {
private final static String SUBNET_PATTERN = "{%subnet}";
private final static String GW_PATTERN = "{%gateway}";
private final static String CTX = FRRPlugin.class.getSimpleName();
private final EventListener<NetworkResourceBundle> subscription;
public FRRPlugin(Context context) {
super(context);
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
}
private void updateSubnets(List<IPv4Subnet> subnets) {
for (var entry : config.instances) {
SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX);
try (var shell = new ShellExecutor(entry.shellConfig)) {
SystemLogger.message("Fetching existing subnets...", CTX);
var existingConfig = executeVTYCommand(new String[]{"show running"}, shell);
var patternOffset = entry.subnetPattern.indexOf(SUBNET_PATTERN);
if (patternOffset == -1) {
SystemLogger.error("Unable to parse FRR config '" + entry.subnetPattern + "' because " + SUBNET_PATTERN + " is not found", CTX);
continue;
}
var startPattern = entry.subnetPattern.substring(0, patternOffset);
var endPattern = entry.subnetPattern.substring(patternOffset + SUBNET_PATTERN.length()).replaceAll(Pattern.quote(GW_PATTERN), entry.gateway);
var existingSubnets = new ArrayList<IPv4Subnet>();
for (var line : existingConfig.split(Pattern.quote("\n"))) {
line = line.trim();
if (line.startsWith(startPattern)) {
var subnetString = line.substring(startPattern.length()).replaceAll(Pattern.quote(endPattern), "").trim();
var subnet = new IPv4Subnet(subnetString);
existingSubnets.add(subnet);
}
}
SystemLogger.message("There is " + existingSubnets.size() + " managed subnets", CTX);
//удаляем лишние подсети
var commandsToRemove = existingSubnets.stream()
.filter(subnet -> !subnets.contains(subnet) && !entry.essentialSubnets.contains(subnet))
.map(subnet -> "no " + entry.subnetPattern
.replaceAll(Pattern.quote(GW_PATTERN), entry.gateway)
.replaceAll(Pattern.quote(SUBNET_PATTERN), subnet.toString()))
.toList();
if (!commandsToRemove.isEmpty()) {
SystemLogger.message( commandsToRemove.size() + " subnets should be removed", CTX);
executeVTYCommandBundle(commandsToRemove, true, shell, count -> SystemLogger.message(count + "/" + commandsToRemove.size() + " subnets has been removed", CTX));
}
//добавляем новые подсети
var commandsToAdd = subnets.stream().filter(subnet -> !existingSubnets.contains(subnet))
.map(subnet -> entry.subnetPattern
.replaceAll(Pattern.quote(GW_PATTERN), entry.gateway)
.replaceAll(Pattern.quote(SUBNET_PATTERN), subnet.toString()))
.toList();
if (!commandsToAdd.isEmpty()) {
SystemLogger.message( commandsToAdd.size() + " subnets should be added", CTX);
executeVTYCommandBundle(commandsToAdd, true, shell, count -> SystemLogger.message(count + "/" + commandsToAdd.size() + " subnets has been added", CTX));
}
SystemLogger.message("FRR update is complete", CTX);
} catch (IOException e) {
SystemLogger.error("Shell error", CTX, e);
}
}
}
private void executeVTYCommandBundle(List<String> commands, boolean configMode, ShellExecutor shell, Consumer<Integer> progressCallback) {
var buffer = new ArrayList<String>();
if (configMode) {
buffer.add("conf t");
}
for (var i = 0; i < commands.size(); i++) {
buffer.add(commands.get(i));
if (i % 100 == 0 && i > 0) {
executeVTYCommand(buffer.toArray(new String[0]), shell);
if (progressCallback != null) {
progressCallback.accept(i);
}
buffer.clear();
if (configMode) {
buffer.add("conf t");
}
}
}
if (!buffer.isEmpty()) {
executeVTYCommand(buffer.toArray(new String[0]), shell);
if (progressCallback != null) {
progressCallback.accept(commands.size());
}
}
}
private String executeVTYCommand(String[] command, ShellExecutor shell) {
var buffer = new ArrayList<String>();
buffer.add("vtysh");
for (var part : command) {
buffer.add("-c");
buffer.add(part);
}
return shell.executeCommand(buffer.toArray(new String[0]));
}
@Override
public void close() throws IOException {
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
}
@JSONSerializable
public static class FRRConfig {
@Getter
@Setter
@JSONArrayProperty(type = Entry.class)
private List<Entry> instances = new ArrayList<>();
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JSONSerializable
public static class Entry {
@Getter
@Setter
@JSONProperty
private ShellExecutor.Config shellConfig = new ShellExecutor.Config();
@Getter
@Setter
@JSONProperty
private String subnetPattern = "ip route {%subnet} {%gateway} 100";
@Getter
@Setter
@JSONProperty
private String gateway = "127.0.0.1";
@Getter
@Setter
@JSONArrayProperty(type = IPv4Subnet.class)
private List<IPv4Subnet> essentialSubnets = new ArrayList<>();
}
}
}

View File

@ -3,6 +3,9 @@ package ru.kirillius.pf.sdn.External.API;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.json.JSONObject;
import org.json.JSONTokener;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig; import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
@ -11,9 +14,9 @@ import ru.kirillius.pf.sdn.core.Util.HashUtil;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.*;
import java.util.StringJoiner;
public class GitSubscription implements SubscriptionProvider { public class GitSubscription implements SubscriptionProvider {
private final Context context; private final Context context;
@ -39,10 +42,27 @@ public class GitSubscription implements SubscriptionProvider {
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX); SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
checkAndPullUpdates(repository); checkAndPullUpdates(repository);
} }
repository.close(); repository.close();
return Map.of();
var resourcesDir = new File(repoDir, "resources");
if (!resourcesDir.exists()) {
SystemLogger.error(resourcesDir + " is not exist in repo (" + config.getSource() + ")", CTX);
return Collections.emptyMap();
}
var map = new HashMap<String, NetworkResourceBundle>();
for (var file : Objects.requireNonNull(resourcesDir.listFiles())) {
try (var stream = new FileInputStream(file)) {
var name = file.getName();
if(!name.endsWith(".json")) {
continue;
}
var bundle = JSONUtility.deserializeStructure(new JSONObject(new JSONTokener(stream)), NetworkResourceBundle.class);
map.put(name.substring(0, name.length() - 5), bundle);
}
}
return map;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@ -0,0 +1,32 @@
Есть альтернативы:
Существует несколько API для получения префиксов (IP-блоков) по номеру ASN. Вот основные варианты:
## 1. **RIPE Stat (RESTful API)**
**URL:** `https://stat.ripe.net/data/announced-prefixes/data.json`
**Пример запроса:**
```bash
curl "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS3333"
```
## 2. **BGPView API**
**URL:** `https://api.bgpview.io/asn/ASN/prefixes`
**Пример:**
```bash
curl "https://api.bgpview.io/asn/AS3333/prefixes"
```
## 3. **IPtoASN API**
**URL:** `https://api.iptoasn.com/v1/as/ip/ASN`
**Пример:**
```bash
curl "https://api.iptoasn.com/v1/as/ip/AS3333"
```
## 5. **Cloudflare Radar API**
**URL:** `https://api.cloudflare.com/client/v4/radar/asn/prefixes`
**Пример:**
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.cloudflare.com/client/v4/radar/asn/prefixes?asn=3333"
```

View File

@ -0,0 +1,118 @@
package ru.kirillius.pf.sdn.External.API;
import lombok.*;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable;
import java.io.IOException;
import java.util.Arrays;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
import static net.schmizz.sshj.common.IOUtils.readFully;
public class ShellExecutor implements Closeable {
private final static String CTX = ShellExecutor.class.getSimpleName();
private final Config config;
private SSHClient sshClient;
public ShellExecutor(Config config) {
this.config = config;
}
public String executeCommand(String[] command) {
var buffer = new StringJoiner(" ");
Arrays.stream(command).forEach(e -> buffer.add('"' + e + '"'));
if (config.useSSH) {
try {
if (sshClient == null) {
sshClient = new SSHClient();
sshClient.addHostKeyVerifier(new PromiscuousVerifier());
sshClient.connect(config.host, config.port);
sshClient.authPassword(config.username, config.password);
}
try (var session = sshClient.startSession()) {
var process = session.exec(buffer.toString());
var output = readFully(process.getInputStream()).toString();
process.join(10, TimeUnit.SECONDS);
return output;
}
} catch (IOException e) {
SystemLogger.error("Failed to execute remote command " + buffer + " via ssh on host " + config.host, CTX, e);
return null;
}
} else {
try {
var runtime = Runtime.getRuntime();
var process = runtime.exec(command);
var output = readFully(process.getInputStream()).toString();
process.waitFor();
return output;
} catch (IOException | InterruptedException e) {
SystemLogger.error("Failed to execute local shell command " + buffer, CTX, e);
return null;
}
}
}
@Override
public void close() throws IOException {
if (sshClient != null) {
sshClient.disconnect();
sshClient.close();
sshClient = null;
}
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JSONSerializable
public static class Config {
@Getter
@Setter
@JSONProperty
private boolean useSSH = false;
@Getter
@Setter
@JSONProperty
private String host = "127.0.0.1";
@Getter
@Setter
@JSONProperty
private int port = 22;
@Getter
@Setter
@JSONProperty
private String username = "root";
@Getter
@Setter
@JSONProperty
private String password = "securepassword";
@Override
public String toString() {
var builder = new StringBuilder();
builder.append("Shell in ");
if (useSSH) {
builder.append("ssh ").append(username).append("@").append(host).append(":").append(port);
} else {
builder.append("localhost");
}
return builder.toString();
}
}
}

View File

@ -0,0 +1,121 @@
package ru.kirillius.pf.sdn.External.API;
import lombok.Getter;
import org.json.JSONObject;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONUtility;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class TechnitiumDNSAPI implements Closeable {
private final String server;
private final String authToken;
private final HttpClient httpClient;
public TechnitiumDNSAPI(String server, String authToken) {
this.server = server;
httpClient = HttpClient.newBuilder().build();
this.authToken = authToken;
}
private JSONObject getRequest(String api, Map<String, String> additionalParams) {
var params = new HashMap<>(additionalParams);
var joiner = new StringJoiner("&");
params.put("token", authToken);
params.forEach((key, value) -> joiner.add(key + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8)));
var url = server + api;
if (!params.isEmpty()) {
url += "?" + joiner;
}
var request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Accept", "application/json").GET().build();
try {
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
var result = new JSONObject(response.body());
if (result.getString("status").equals("ok")) {
return result.getJSONObject("response");
} else {
throw new RuntimeException("API error: " + result.getString("errorMessage"));
}
} else {
throw new RuntimeException("API HTTP error: " + response.statusCode());
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException("API internal error", e);
}
}
public enum ZoneType {
Primary, Secondary, Stub, Forwarder, SecondaryForwarder, Catalog, SecondaryCatalog
}
@JSONSerializable
public static class ZoneResponse {
@JSONProperty
@Getter
private String name;
@JSONProperty
@Getter
private ZoneType type;
@JSONProperty(required = false)
@Getter
private String dnssecStatus;
@JSONProperty
@Getter
private long soaSerial;
@JSONProperty(required = false)
@Getter
private Date expiry;
@JSONProperty(required = false)
@Getter
private boolean isExpired;
@JSONProperty(required = false)
@Getter
private boolean syncFailed;
@JSONProperty
@Getter
private boolean disabled;
@JSONProperty
@Getter
private Date lastModified;
}
public List<ZoneResponse> getZones() {
var request = getRequest("/api/zones/list", Collections.emptyMap());
return JSONUtility.deserializeCollection(request.getJSONArray("zones"), ZoneResponse.class, null).toList();
}
public void createForwarderZone(String zoneName, String forwarder) {
var params = new HashMap<String, String>();
params.put("zone", zoneName);
params.put("type", ZoneType.Forwarder.name());
params.put("forwarder", forwarder);
getRequest("/api/zones/create", params);
}
public void deleteZone(String zoneName) {
var params = new HashMap<String, String>();
params.put("zone", zoneName);
getRequest("/api/zones/delete", params);
}
@Override
public void close() throws IOException {
httpClient.close();
}
}

View File

@ -0,0 +1,93 @@
package ru.kirillius.pf.sdn.External.API;
import lombok.*;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.json.JSONArrayProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.pf.sdn.core.AbstractPlugin;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public final class TechnitiumPlugin extends AbstractPlugin<TechnitiumPlugin.TechnitiumConfig> {
private final static String CTX = TechnitiumPlugin.class.getSimpleName();
private final EventListener<NetworkResourceBundle> subscription;
public TechnitiumPlugin(Context context) {
super(context);
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
}
private void updateSubnets(List<String> domains) {
for (var instance : config.instances) {
SystemLogger.message("Updating zones on DNS server " + instance.server, CTX);
try (var api = new TechnitiumDNSAPI(instance.server, instance.token)) {
var existingForwardZones = api.getZones().stream()
.filter(zoneResponse -> zoneResponse.getType() == TechnitiumDNSAPI.ZoneType.Forwarder)
.map(TechnitiumDNSAPI.ZoneResponse::getName)
.collect(Collectors.toCollection(ArrayList::new));
existingForwardZones.forEach(zoneName -> {
if (!domains.contains(zoneName)) {
//delete zone
SystemLogger.message("Deleting zone " + zoneName, CTX);
api.deleteZone(zoneName);
}
});
domains.forEach(zoneName -> {
if (!existingForwardZones.contains(zoneName)) {
SystemLogger.message("Creating FWD zone " + zoneName, CTX);
api.createForwarderZone(zoneName, instance.forwarder);
}
});
} catch (IOException e) {
SystemLogger.error("Error happened on DNS server " + instance.server + " sync", CTX, e);
}
}
}
@Override
public void close() throws IOException {
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
}
@JSONSerializable
public static class TechnitiumConfig {
@Getter
@Setter
@JSONArrayProperty(type = Entry.class)
private List<Entry> instances = new ArrayList<>();
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JSONSerializable
public static class Entry {
@Getter
@Setter
@JSONProperty
private String forwarder = "127.0.0.1";
@Getter
@Setter
@JSONProperty
private String server = "http://127.0.0.1:5380";
@Getter
@Setter
@JSONProperty
private String token = "notoken";
}
}
}

View File

@ -0,0 +1,12 @@
package ru.kirillius.pf.sdn.core;
public abstract class AbstractPlugin<CT> implements Plugin<CT> {
protected final CT config;
protected final Context context;
@SuppressWarnings({"unchecked", "rawtypes"})
public AbstractPlugin(Context context) {
config = (CT) context.getConfig().getPluginsConfig().getConfig((Class) getClass());
this.context = context;
}
}

View File

@ -5,15 +5,13 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONTokener; import org.json.JSONTokener;
import ru.kirillius.json.JSONArrayProperty; import ru.kirillius.json.*;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.pf.sdn.core.Auth.AuthToken; import ru.kirillius.pf.sdn.core.Auth.AuthToken;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig; import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import java.io.*; import java.io.*;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -43,6 +41,22 @@ public class Config {
@JSONArrayProperty(type = RepositoryConfig.class) @JSONArrayProperty(type = RepositoryConfig.class)
private List<RepositoryConfig> subscriptions = Collections.emptyList(); private List<RepositoryConfig> subscriptions = Collections.emptyList();
@Setter
@Getter
@JSONArrayProperty(type = String.class)
private List<String> subscribedResources = Collections.emptyList();
@Setter
@Getter
@JSONProperty(required = false)
private PluginConfigStorage pluginsConfig = new PluginConfigStorage();
@Setter
@Getter
@JSONArrayProperty(type = Class.class, required = false)
private List<Class<? extends Plugin<?>>> enabledPlugins = new ArrayList<>();
@Setter @Setter
@Getter @Getter
@JSONProperty @JSONProperty
@ -66,7 +80,7 @@ public class Config {
@Setter @Setter
@Getter @Getter
@JSONProperty @JSONProperty
private int mergeSubnetsCount = 10; private int mergeSubnetsWithUsage = 51;
@Setter @Setter
@Getter @Getter

View File

@ -4,6 +4,7 @@ import org.eclipse.jetty.server.Server;
import ru.kirillius.pf.sdn.core.Auth.AuthManager; import ru.kirillius.pf.sdn.core.Auth.AuthManager;
import ru.kirillius.pf.sdn.core.Networking.ASInfoService; import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
import ru.kirillius.pf.sdn.core.Networking.NetworkManager; import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
public interface Context { public interface Context {
Config getConfig(); Config getConfig();
@ -16,4 +17,5 @@ public interface Context {
NetworkManager getNetworkManager(); NetworkManager getNetworkManager();
ContextEventsHandler getEventsHandler(); ContextEventsHandler getEventsHandler();
SubscriptionManager getSubscriptionManager();
} }

View File

@ -1,6 +1,7 @@
package ru.kirillius.pf.sdn.core.Networking; package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter; import lombok.Getter;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONSerializer; import ru.kirillius.json.JSONSerializer;
import ru.kirillius.json.SerializationException; import ru.kirillius.json.SerializationException;
import ru.kirillius.pf.sdn.core.Util.IPv4Util; import ru.kirillius.pf.sdn.core.Util.IPv4Util;
@ -8,7 +9,7 @@ import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@JSONSerializable(IPv4Subnet.Serializer.class)
public class IPv4Subnet { public class IPv4Subnet {
public final static class Serializer implements JSONSerializer<IPv4Subnet> { public final static class Serializer implements JSONSerializer<IPv4Subnet> {
@ -24,7 +25,8 @@ public class IPv4Subnet {
} }
} }
private final long address; @Getter
private final long longAddress;
@Getter @Getter
private final int prefixLength; private final int prefixLength;
@ -33,12 +35,12 @@ public class IPv4Subnet {
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
IPv4Subnet that = (IPv4Subnet) o; IPv4Subnet that = (IPv4Subnet) o;
return address == that.address && prefixLength == that.prefixLength; return longAddress == that.longAddress && prefixLength == that.prefixLength;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(address, prefixLength); return Objects.hash(longAddress, prefixLength);
} }
public IPv4Subnet(String subnet) { public IPv4Subnet(String subnet) {
@ -49,20 +51,28 @@ public class IPv4Subnet {
var prefix = Integer.parseInt(split[1]); var prefix = Integer.parseInt(split[1]);
IPv4Util.validatePrefix(prefix); IPv4Util.validatePrefix(prefix);
address = IPv4Util.ipAddressToLong(split[0]); longAddress = IPv4Util.ipAddressToLong(split[0]);
prefixLength = prefix; prefixLength = prefix;
} }
public IPv4Subnet(long longAddress, int prefixLength) {
this.longAddress = longAddress;
this.prefixLength = prefixLength;
}
public IPv4Subnet(String address, int prefixLength) { public IPv4Subnet(String address, int prefixLength) {
IPv4Util.validatePrefix(prefixLength); IPv4Util.validatePrefix(prefixLength);
this.address = IPv4Util.ipAddressToLong(address); this.longAddress = IPv4Util.ipAddressToLong(address);
this.prefixLength = prefixLength; this.prefixLength = prefixLength;
} }
public long count() {
return IPv4Util.calculateCountForPrefixLength(prefixLength);
}
public String getAddress() { public String getAddress() {
return IPv4Util.longToIpAddress(address); return IPv4Util.longToIpAddress(longAddress);
} }
@Override @Override
@ -77,7 +87,7 @@ public class IPv4Subnet {
return false; //can't overlap larger prefix return false; //can't overlap larger prefix
} }
return (address & commonMask) == (subnet.address & commonMask); return (longAddress & commonMask) == (subnet.longAddress & commonMask);
} }

View File

@ -2,6 +2,8 @@ package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter; import lombok.Getter;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -12,8 +14,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class NetworkManager implements Closeable { public class NetworkManager implements Closeable {
private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final static String CTX = NetworkManager.class.getSimpleName();
private Context context; private final Context context;
public NetworkManager(Context context) { public NetworkManager(Context context) {
this.context = context; this.context = context;
@ -33,28 +35,43 @@ public class NetworkManager implements Closeable {
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>(); private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
public synchronized void triggerUpdate() { public void triggerUpdate() {
if (isUpdatingNow()) { if (isUpdatingNow()) {
return; return;
} }
SystemLogger.message("Updating network manager", CTX);
updateProcess.set(executor.submit(() -> { updateProcess.set(executor.submit(() -> {
SystemLogger.message("Update is started", CTX);
var config = context.getConfig(); var config = context.getConfig();
var filteredResources = config.getFilteredResources(); var filteredResources = config.getFilteredResources();
var asn = new ArrayList<>(inputResources.getASN()); var asn = new ArrayList<>(inputResources.getASN());
asn.removeAll(filteredResources.getASN()); asn.removeAll(filteredResources.getASN());
if (cacheInvalid.get()) { if (cacheInvalid.get()) {
fetchPrefixes(asn); fetchPrefixes(asn);
} }
var subnets = new HashSet<>(inputResources.getSubnets()); var subnets = new HashSet<>(inputResources.getSubnets());
prefixCache.values().forEach(subnets::addAll); asn.forEach(n -> {
var cached = prefixCache.get(n);
if (cached == null) {
return;
}
subnets.addAll(cached);
SystemLogger.message("Using " + cached.size() + " subnets from AS" + n, CTX);
});
filteredResources.getSubnets().forEach(subnets::remove); filteredResources.getSubnets().forEach(subnets::remove);
SystemLogger.message("Trying to summary " + subnets.size() + " subnets...", CTX);
var merged = IPv4Util.summarySubnets(subnets, config.getMergeSubnetsWithUsage());
SystemLogger.message(merged.getMergedSubnets().size() + " subnets has been merged to " + merged.getResult().size() + " new subnets", CTX);
var domains = new HashSet<>(inputResources.getDomains()); var domains = new HashSet<>(inputResources.getDomains());
filteredResources.getDomains().forEach(domains::remove); filteredResources.getDomains().forEach(domains::remove);
//check overlaps //check domain overlaps
var domainsToRemove = new HashSet<String>(); var domainsToRemove = new HashSet<String>();
for (var domainToMatch : domains) { for (var domainToMatch : domains) {
@ -69,9 +86,11 @@ public class NetworkManager implements Closeable {
domains.removeAll(domainsToRemove); domains.removeAll(domainsToRemove);
outputResources.setASN(Collections.unmodifiableList(asn)); outputResources.setASN(Collections.unmodifiableList(asn));
outputResources.setSubnets(subnets.stream().toList()); outputResources.setSubnets(merged.getResult());
outputResources.setDomains(domains.stream().toList()); outputResources.setDomains(domains.stream().toList());
SystemLogger.message("Update is complete", CTX);
try { try {
context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources); context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources);
} catch (Exception e) { } catch (Exception e) {
@ -80,12 +99,14 @@ public class NetworkManager implements Closeable {
})); }));
} }
public void invalidateCache() { public void invalidateCache() {
cacheInvalid.set(true); cacheInvalid.set(true);
} }
private void fetchPrefixes(List<Integer> systems) { private void fetchPrefixes(List<Integer> systems) {
systems.forEach(as -> { systems.forEach(as -> {
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
var service = context.getASInfoService(); var service = context.getASInfoService();
var future = service.getPrefixes(as); var future = service.getPrefixes(as);

View File

@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn.core.Networking;
import lombok.*; import lombok.*;
import ru.kirillius.json.JSONArrayProperty; import ru.kirillius.json.JSONArrayProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable; import ru.kirillius.json.JSONSerializable;
import java.util.ArrayList; import java.util.ArrayList;
@ -12,6 +13,10 @@ import java.util.List;
@Builder @Builder
@JSONSerializable @JSONSerializable
public class NetworkResourceBundle { public class NetworkResourceBundle {
@Getter
@Setter
@JSONProperty(required = false)
private String description = "";
@Getter @Getter
@Setter @Setter
@JSONArrayProperty(type = Integer.class) @JSONArrayProperty(type = Integer.class)

View File

@ -0,0 +1,20 @@
package ru.kirillius.pf.sdn.core;
import lombok.SneakyThrows;
import java.io.Closeable;
import java.lang.reflect.ParameterizedType;
public interface Plugin<CT> extends Closeable {
@SuppressWarnings("unchecked")
static <T> Class<T> getConfigClass(Class<? extends Plugin<T>> pluginClass) {
var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
var typeArguments = genericSuperclass.getActualTypeArguments();
return (Class<T>) typeArguments[0];
}
@SneakyThrows
static <T extends Plugin<?>> T loadPlugin(Class<T> pluginClass, Context context) {
return pluginClass.getConstructor(Context.class).newInstance(context);
}
}

View File

@ -0,0 +1,61 @@
package ru.kirillius.pf.sdn.core;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.json.JSONObject;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONSerializer;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.SerializationException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@NoArgsConstructor
@JSONSerializable(PluginConfigStorage.Serializer.class)
public class PluginConfigStorage {
public final static class Serializer implements JSONSerializer<PluginConfigStorage> {
@Override
public Object serialize(PluginConfigStorage pluginConfigStorage) throws SerializationException {
var json = new JSONObject();
pluginConfigStorage.configs.forEach((key, value) -> {
json.put(key.getName(), JSONUtility.serializeStructure(value));
});
return json;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public PluginConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException {
var loader = getClass().getClassLoader();
var json = (JSONObject) o;
var storage = new PluginConfigStorage();
json.keySet().forEach(key -> {
try {
var pluginClass = loader.loadClass(key);
var value = json.getJSONObject(key);
var configClass = Plugin.getConfigClass((Class) pluginClass);
storage.configs.put((Class)pluginClass, JSONUtility.deserializeStructure(value, configClass));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
});
return storage;
}
}
private final Map<Class<? extends Plugin<?>>, Object> configs = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
@SneakyThrows
public <CT> CT getConfig(Class<? extends Plugin<CT>> pluginClass) {
if (!configs.containsKey(pluginClass)) {
var configClass = Plugin.getConfigClass(pluginClass);
var instance = configClass.getConstructor().newInstance();
configs.put(pluginClass, instance);
}
return (CT) configs.get(pluginClass);
}
}

View File

@ -6,6 +6,8 @@ import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@ -14,7 +16,9 @@ import java.util.concurrent.atomic.AtomicReference;
public class SubscriptionManager implements Closeable { public class SubscriptionManager implements Closeable {
private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final ExecutorService executor = Executors.newSingleThreadExecutor();
private Context context; private final Context context;
private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
public SubscriptionManager(Context context) { public SubscriptionManager(Context context) {
this.context = context; this.context = context;
@ -38,13 +42,35 @@ public class SubscriptionManager implements Closeable {
updateProcess.set(executor.submit(() -> { updateProcess.set(executor.submit(() -> {
var bundle = new NetworkResourceBundle(); var bundle = new NetworkResourceBundle();
var config = context.getConfig();
var subscribedResources = config.getSubscribedResources();
for (var repoConfig : config.getSubscriptions()) {
var providerType = repoConfig.getType();
var provider = providerCache.get(providerType);
if (provider == null) {
//noinspection unchecked
provider = SubscriptionProvider.instantiate((Class<? extends SubscriptionProvider>) providerType, context);
//noinspection unchecked
providerCache.put((Class<? extends SubscriptionProvider>) providerType, provider);
}
var resources = provider.getResources(repoConfig);
resources.keySet().forEach(key -> {
var resourceName = repoConfig.getName() + ":" + key;
//добавляем только выбранные ресурсы
if (subscribedResources.contains(resourceName)) {
bundle.add(resources.get(key));
}
});
}
outputResources.clear(); outputResources.clear();
outputResources.add(bundle); outputResources.add(bundle);
try { try {
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources); context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e); //FIXME to LOG
} }
})); }));
} }

View File

@ -1,9 +1,15 @@
package ru.kirillius.pf.sdn.core.Util; package ru.kirillius.pf.sdn.core.Util;
import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
import java.net.InetAddress; 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.regex.Pattern;
import java.util.stream.Collectors;
public class IPv4Util { public class IPv4Util {
@ -50,4 +56,219 @@ public class IPv4Util {
} }
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF); return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
} }
public interface SummarisationResult {
List<IPv4Subnet> getResult();
Set<IPv4Subnet> getMergedSubnets();
}
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<>();
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));
}
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);
}
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++;
}
}
}
}
private int findMinPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
}
private int findMaxPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
}
private long findMinAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
}
private long findMaxAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
}
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;
}
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;
}
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());
}
}
public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) {
return new SubnetSummaryUtility(subnets, usePercentage);
}
private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), prefixLength);
}
public static long calculateCountForPrefixLength(long prefixLength) {
return 1L << (32L - prefixLength);
}
} }

View File

@ -0,0 +1,42 @@
package ru.kirillius.pf.sdn.core.Util;
import org.junit.jupiter.api.Test;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
import java.util.ArrayList;
import static org.assertj.core.api.Assertions.assertThat;
class IPv4UtilTest {
@Test
void summarySubnets() {
var subnets = new ArrayList<IPv4Subnet>();
for (var i = 0; i <= 254; i++) {
subnets.add(new IPv4Subnet("192.168." + i + ".0", 24));
}
subnets.remove(0);
for (var i = 1; i <= 255; i++) {
subnets.add(new IPv4Subnet("192.168.255." + i, 32));
}
for (var i = 0; i <= 254/3; i++) {
subnets.add(new IPv4Subnet("192.169." + i + ".0", 24));
}
subnets.add(new IPv4Subnet("1.1.1.1/32"));
subnets.add(new IPv4Subnet("200.1.1.1/32"));
//subnets.forEach(System.out::println);
var merged = IPv4Util.summarySubnets(subnets, 51).getResult();
merged.forEach(System.out::println);
assertThat(merged).isNotNull();
}
}