diff --git a/app/pom.xml b/app/pom.xml
index 0fcf3dc..9b848e9 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -32,6 +32,14 @@
org.eclipse.jgit
7.3.0.202506031305-r
+
+
+
+ com.hierynomus
+ sshj
+ 0.39.0
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/App.java b/app/src/main/java/ru/kirillius/pf/sdn/App.java
index 73bf782..f52638c 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/App.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java
@@ -1,24 +1,26 @@
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.Subscription.RepositoryConfig;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
-import java.util.Map;
import java.util.logging.Level;
public class App extends AppContext {
private final static File configFile = new File("config.json");
+ @SneakyThrows
public App(File configFile) {
super(configFile);
- GitSubscription subscription = new GitSubscription(this);
- RepositoryConfig repositoryConfig = new RepositoryConfig("test", GitSubscription.class, "https://git.kirillius.ru/kirillius/docker-decompose.git");
- Map resources = subscription.getResources(repositoryConfig);
+
+
+ getSubscriptionManager().triggerUpdate();
+
+
}
static {
@@ -28,7 +30,15 @@ public class App extends AppContext {
public static void main(String[] args) {
try (App app = new App(configFile)) {
-
+ app.getEventsHandler().getNetworkManagerUpdateEvent().add(new EventListener() {
+ @Override
+ public void invoke(NetworkResourceBundle bundle) throws Exception {
+ SystemLogger.message("Network resource bundle updated.", CTX);
+ }
+ });
+ while (true) {
+ Thread.yield();
+ }
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java b/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java
index 2b3ab90..dc0077b 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java
@@ -9,13 +9,20 @@ import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.ContextEventsHandler;
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
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.File;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
public class AppContext implements Context, Closeable {
+ protected final static String CTX = AppContext.class.getSimpleName();
+
public AppContext(File configFile) {
try {
config = Config.load(configFile);
@@ -34,6 +41,30 @@ public class AppContext implements Context, Closeable {
ASInfoService.setProvider(new HEInfoProvider(this));
networkManager = new NetworkManager(this);
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> 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;
@Getter
private final ASInfoService ASInfoService;
+ @Getter
+ private final SubscriptionManager subscriptionManager;
@Override
public void close() throws IOException {
+ loadedPlugins.forEach(plugin -> {
+ try {
+ plugin.close();
+ } catch (IOException e) {
+ SystemLogger.error("Error closing plugin", CTX, e);
+ }
+ });
ASInfoService.close();
networkManager.close();
try {
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/FRRPlugin.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/FRRPlugin.java
new file mode 100644
index 0000000..a9f73e1
--- /dev/null
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/FRRPlugin.java
@@ -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 {
+ 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 subscription;
+
+ public FRRPlugin(Context context) {
+ super(context);
+ subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
+ }
+
+ private void updateSubnets(List 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();
+ 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 commands, boolean configMode, ShellExecutor shell, Consumer progressCallback) {
+
+ var buffer = new ArrayList();
+ 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();
+ 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 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 essentialSubnets = new ArrayList<>();
+ }
+ }
+}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java
index edbe1b9..6f233e9 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java
@@ -3,6 +3,9 @@ package ru.kirillius.pf.sdn.External.API;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
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.Networking.NetworkResourceBundle;
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 java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
-import java.util.Map;
-import java.util.StringJoiner;
+import java.util.*;
public class GitSubscription implements SubscriptionProvider {
private final Context context;
@@ -39,10 +42,27 @@ public class GitSubscription implements SubscriptionProvider {
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
checkAndPullUpdates(repository);
}
-
-
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();
+ 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) {
throw new RuntimeException(e);
}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/INFO.md b/app/src/main/java/ru/kirillius/pf/sdn/External/API/INFO.md
new file mode 100644
index 0000000..dd51794
--- /dev/null
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/INFO.md
@@ -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"
+```
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java
new file mode 100644
index 0000000..c76a4a2
--- /dev/null
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java
@@ -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();
+ }
+ }
+}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumDNSAPI.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumDNSAPI.java
new file mode 100644
index 0000000..17e4004
--- /dev/null
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumDNSAPI.java
@@ -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 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 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();
+ 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();
+ params.put("zone", zoneName);
+ getRequest("/api/zones/delete", params);
+ }
+
+
+ @Override
+ public void close() throws IOException {
+ httpClient.close();
+ }
+}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumPlugin.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumPlugin.java
new file mode 100644
index 0000000..13a268e
--- /dev/null
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumPlugin.java
@@ -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 {
+
+ private final static String CTX = TechnitiumPlugin.class.getSimpleName();
+ private final EventListener subscription;
+
+ public TechnitiumPlugin(Context context) {
+ super(context);
+ subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
+ }
+
+ private void updateSubnets(List 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 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";
+ }
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractPlugin.java b/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractPlugin.java
new file mode 100644
index 0000000..ab6baca
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractPlugin.java
@@ -0,0 +1,12 @@
+package ru.kirillius.pf.sdn.core;
+
+public abstract class AbstractPlugin implements Plugin {
+ 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;
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java
index 3d5eb75..23f1419 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java
@@ -5,15 +5,13 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import org.json.JSONObject;
import org.json.JSONTokener;
-import ru.kirillius.json.JSONArrayProperty;
-import ru.kirillius.json.JSONProperty;
-import ru.kirillius.json.JSONSerializable;
-import ru.kirillius.json.JSONUtility;
+import ru.kirillius.json.*;
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import java.io.*;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -43,6 +41,22 @@ public class Config {
@JSONArrayProperty(type = RepositoryConfig.class)
private List subscriptions = Collections.emptyList();
+ @Setter
+ @Getter
+ @JSONArrayProperty(type = String.class)
+ private List subscribedResources = Collections.emptyList();
+
+
+ @Setter
+ @Getter
+ @JSONProperty(required = false)
+ private PluginConfigStorage pluginsConfig = new PluginConfigStorage();
+
+ @Setter
+ @Getter
+ @JSONArrayProperty(type = Class.class, required = false)
+ private List>> enabledPlugins = new ArrayList<>();
+
@Setter
@Getter
@JSONProperty
@@ -66,7 +80,7 @@ public class Config {
@Setter
@Getter
@JSONProperty
- private int mergeSubnetsCount = 10;
+ private int mergeSubnetsWithUsage = 51;
@Setter
@Getter
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java
index 8a88472..1a56852 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java
@@ -4,6 +4,7 @@ import org.eclipse.jetty.server.Server;
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
+import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
public interface Context {
Config getConfig();
@@ -16,4 +17,5 @@ public interface Context {
NetworkManager getNetworkManager();
ContextEventsHandler getEventsHandler();
+ SubscriptionManager getSubscriptionManager();
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java
index d4bd9a2..e098d5e 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java
@@ -1,6 +1,7 @@
package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter;
+import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONSerializer;
import ru.kirillius.json.SerializationException;
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.regex.Pattern;
-
+@JSONSerializable(IPv4Subnet.Serializer.class)
public class IPv4Subnet {
public final static class Serializer implements JSONSerializer {
@@ -24,7 +25,8 @@ public class IPv4Subnet {
}
}
- private final long address;
+ @Getter
+ private final long longAddress;
@Getter
private final int prefixLength;
@@ -33,12 +35,12 @@ public class IPv4Subnet {
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
IPv4Subnet that = (IPv4Subnet) o;
- return address == that.address && prefixLength == that.prefixLength;
+ return longAddress == that.longAddress && prefixLength == that.prefixLength;
}
@Override
public int hashCode() {
- return Objects.hash(address, prefixLength);
+ return Objects.hash(longAddress, prefixLength);
}
public IPv4Subnet(String subnet) {
@@ -49,20 +51,28 @@ public class IPv4Subnet {
var prefix = Integer.parseInt(split[1]);
IPv4Util.validatePrefix(prefix);
- address = IPv4Util.ipAddressToLong(split[0]);
+ longAddress = IPv4Util.ipAddressToLong(split[0]);
prefixLength = prefix;
}
+ public IPv4Subnet(long longAddress, int prefixLength) {
+ this.longAddress = longAddress;
+ this.prefixLength = prefixLength;
+ }
public IPv4Subnet(String address, int prefixLength) {
IPv4Util.validatePrefix(prefixLength);
- this.address = IPv4Util.ipAddressToLong(address);
+ this.longAddress = IPv4Util.ipAddressToLong(address);
this.prefixLength = prefixLength;
}
+ public long count() {
+ return IPv4Util.calculateCountForPrefixLength(prefixLength);
+ }
+
public String getAddress() {
- return IPv4Util.longToIpAddress(address);
+ return IPv4Util.longToIpAddress(longAddress);
}
@Override
@@ -77,7 +87,7 @@ public class IPv4Subnet {
return false; //can't overlap larger prefix
}
- return (address & commonMask) == (subnet.address & commonMask);
+ return (longAddress & commonMask) == (subnet.longAddress & commonMask);
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java
index f96083c..fef2417 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java
@@ -2,6 +2,8 @@ package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter;
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.IOException;
@@ -12,8 +14,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class NetworkManager implements Closeable {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
-
- private Context context;
+ private final static String CTX = NetworkManager.class.getSimpleName();
+ private final Context context;
public NetworkManager(Context context) {
this.context = context;
@@ -33,28 +35,43 @@ public class NetworkManager implements Closeable {
private final Map> prefixCache = new ConcurrentHashMap<>();
- public synchronized void triggerUpdate() {
+ public void triggerUpdate() {
if (isUpdatingNow()) {
return;
}
+ SystemLogger.message("Updating network manager", CTX);
+
updateProcess.set(executor.submit(() -> {
+ SystemLogger.message("Update is started", CTX);
var config = context.getConfig();
var filteredResources = config.getFilteredResources();
var asn = new ArrayList<>(inputResources.getASN());
asn.removeAll(filteredResources.getASN());
-
if (cacheInvalid.get()) {
fetchPrefixes(asn);
}
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);
+ 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());
filteredResources.getDomains().forEach(domains::remove);
- //check overlaps
+ //check domain overlaps
var domainsToRemove = new HashSet();
for (var domainToMatch : domains) {
@@ -69,9 +86,11 @@ public class NetworkManager implements Closeable {
domains.removeAll(domainsToRemove);
outputResources.setASN(Collections.unmodifiableList(asn));
- outputResources.setSubnets(subnets.stream().toList());
+ outputResources.setSubnets(merged.getResult());
outputResources.setDomains(domains.stream().toList());
+ SystemLogger.message("Update is complete", CTX);
+
try {
context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources);
} catch (Exception e) {
@@ -80,12 +99,14 @@ public class NetworkManager implements Closeable {
}));
}
+
public void invalidateCache() {
cacheInvalid.set(true);
}
private void fetchPrefixes(List systems) {
systems.forEach(as -> {
+ SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
var service = context.getASInfoService();
var future = service.getPrefixes(as);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java
index c29939d..37a89e3 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java
@@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn.core.Networking;
import lombok.*;
import ru.kirillius.json.JSONArrayProperty;
+import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import java.util.ArrayList;
@@ -12,6 +13,10 @@ import java.util.List;
@Builder
@JSONSerializable
public class NetworkResourceBundle {
+ @Getter
+ @Setter
+ @JSONProperty(required = false)
+ private String description = "";
@Getter
@Setter
@JSONArrayProperty(type = Integer.class)
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Plugin.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Plugin.java
new file mode 100644
index 0000000..24b1451
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Plugin.java
@@ -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 extends Closeable {
+ @SuppressWarnings("unchecked")
+ static Class getConfigClass(Class extends Plugin> pluginClass) {
+ var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
+ var typeArguments = genericSuperclass.getActualTypeArguments();
+ return (Class) typeArguments[0];
+ }
+
+ @SneakyThrows
+ static > T loadPlugin(Class pluginClass, Context context) {
+ return pluginClass.getConstructor(Context.class).newInstance(context);
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java b/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java
new file mode 100644
index 0000000..d2e410b
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java
@@ -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 {
+
+ @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>, Object> configs = new ConcurrentHashMap<>();
+
+ @SuppressWarnings("unchecked")
+ @SneakyThrows
+ public CT getConfig(Class extends Plugin> pluginClass) {
+ if (!configs.containsKey(pluginClass)) {
+ var configClass = Plugin.getConfigClass(pluginClass);
+ var instance = configClass.getConstructor().newInstance();
+ configs.put(pluginClass, instance);
+ }
+ return (CT) configs.get(pluginClass);
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java
index 7f70070..b6f00de 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java
@@ -6,6 +6,8 @@ import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import java.io.Closeable;
import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -14,7 +16,9 @@ import java.util.concurrent.atomic.AtomicReference;
public class SubscriptionManager implements Closeable {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
- private Context context;
+ private final Context context;
+
+ private final Map, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
public SubscriptionManager(Context context) {
this.context = context;
@@ -38,13 +42,35 @@ public class SubscriptionManager implements Closeable {
updateProcess.set(executor.submit(() -> {
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.add(bundle);
try {
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
} catch (Exception e) {
- throw new RuntimeException(e);
+ throw new RuntimeException(e); //FIXME to LOG
}
}));
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java
index 358ee3c..f34113a 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java
@@ -1,9 +1,15 @@
package ru.kirillius.pf.sdn.core.Util;
+import lombok.Getter;
import lombok.SneakyThrows;
+import ru.kirillius.pf.sdn.core.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;
public class IPv4Util {
@@ -50,4 +56,219 @@ public class IPv4Util {
}
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
}
+
+ public interface SummarisationResult {
+ List getResult();
+
+ Set getMergedSubnets();
+ }
+
+ private static class SubnetSummaryUtility implements SummarisationResult {
+
+ @Getter
+ private final List result;
+ private final Collection source;
+ @Getter
+ private final Set mergedSubnets = new HashSet<>();
+
+
+ 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));
+ }
+
+ 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);
+ }
+
+ 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 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;
+ }
+
+ 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;
+ }
+
+ 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 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);
+ }
+
+
}
diff --git a/core/src/test/java/ru/kirillius/pf/sdn/core/Util/IPv4UtilTest.java b/core/src/test/java/ru/kirillius/pf/sdn/core/Util/IPv4UtilTest.java
new file mode 100644
index 0000000..5497db2
--- /dev/null
+++ b/core/src/test/java/ru/kirillius/pf/sdn/core/Util/IPv4UtilTest.java
@@ -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();
+
+
+ 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();
+ }
+}
\ No newline at end of file