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> 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> 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) providerType, context); + //noinspection unchecked + providerCache.put((Class) 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