Compare commits

..

3 Commits

Author SHA1 Message Date
kirillius ee3f4f3fe5 WIP работа над resolver 2026-05-17 21:08:23 +03:00
kirillius c1dbaa5bfc WIP: auto resolver 2026-05-17 13:40:33 +03:00
kirillius c0d5fb5d1f WIP работа на resolver 2026-05-14 19:22:38 +03:00
14 changed files with 392 additions and 81 deletions

View File

@ -14,6 +14,7 @@ import ru.kirillius.pf.sdn.core.Auth.AuthManager;
import ru.kirillius.pf.sdn.core.Auth.TokenService; import ru.kirillius.pf.sdn.core.Auth.TokenService;
import ru.kirillius.pf.sdn.core.Networking.BGPInfoService; import ru.kirillius.pf.sdn.core.Networking.BGPInfoService;
import ru.kirillius.pf.sdn.core.Networking.NetworkingService; import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
import ru.kirillius.pf.sdn.core.Networking.ResolverService;
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig; import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService; import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.core.Util.Wait; import ru.kirillius.pf.sdn.core.Util.Wait;
@ -80,7 +81,7 @@ public class App implements Context, Closeable {
* Instantiates all application services and performs initial wiring. * Instantiates all application services and performs initial wiring.
*/ */
private ServiceManager loadServiceManager() { private ServiceManager loadServiceManager() {
var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class)); var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class, ResolverService.class));
var infoService = manager.getService(BGPInfoService.class); var infoService = manager.getService(BGPInfoService.class);
infoService.addProvider(new HEInfoProvider()); infoService.addProvider(new HEInfoProvider());
infoService.addProvider(new RIPEInfoProvider()); infoService.addProvider(new RIPEInfoProvider());

View File

@ -87,6 +87,7 @@ public class SubscriptionManager implements RPC {
@JRPCArgument(name = "ASN") JSONArray ASN, @JRPCArgument(name = "ASN") JSONArray ASN,
@JRPCArgument(name = "subnets") JSONArray subnets, @JRPCArgument(name = "subnets") JSONArray subnets,
@JRPCArgument(name = "addresses") JSONArray addresses, @JRPCArgument(name = "addresses") JSONArray addresses,
@JRPCArgument(name = "autoResolve") boolean autoResolve,
@JRPCArgument(name = "storage") String storage, @JRPCArgument(name = "storage") String storage,
@JRPCArgument(name = "subscribe") boolean subscribe) throws IOException { @JRPCArgument(name = "subscribe") boolean subscribe) throws IOException {
var repositoryConfig = findLocalRepo(storage); var repositoryConfig = findLocalRepo(storage);
@ -113,6 +114,7 @@ public class SubscriptionManager implements RPC {
.subnets(merged) .subnets(merged)
.domains(domainList) .domains(domainList)
.description(description) .description(description)
.resolveDomains(autoResolve)
.build() .build()
).toString(2)); ).toString(2));
} }
@ -124,7 +126,6 @@ public class SubscriptionManager implements RPC {
context.getServiceManager().getService(SubscriptionService.class).triggerUpdate(); context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
} }
private RepositoryConfig findLocalRepo(String storage) throws FileNotFoundException { private RepositoryConfig findLocalRepo(String storage) throws FileNotFoundException {
var optional = context.getConfig().getSubscriptions().stream().filter(repositoryConfig -> repositoryConfig.getType().equals(LocalFilesystemSubscription.class) && repositoryConfig.getName().equals(storage)).findFirst(); var optional = context.getConfig().getSubscriptions().stream().filter(repositoryConfig -> repositoryConfig.getType().equals(LocalFilesystemSubscription.class) && repositoryConfig.getName().equals(storage)).findFirst();
if (optional.isEmpty()) { if (optional.isEmpty()) {
@ -134,7 +135,6 @@ public class SubscriptionManager implements RPC {
return optional.get(); return optional.get();
} }
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public void removeLocalResourceFile( public void removeLocalResourceFile(

View File

@ -40,6 +40,37 @@ public class Config {
@JSONProperty @JSONProperty
private volatile boolean cachingAS = true; private volatile boolean cachingAS = true;
@Getter
@Setter
@JSONProperty(required = false)
private volatile boolean cachingDomains = true;
@Getter
@Setter
@JSONArrayProperty(type = String.class, required = false)
private volatile List<String> domainResolvers = List.of("8.8.8.8", "77.88.8.8");
/**
* Time in minutes
*/
@Getter
@Setter
@JSONProperty(required = false)
private volatile int domainLookupInterval = 5;
// @Getter
// @Setter
// @JSONProperty(required = false)
// private volatile int autoLookupPrefixLength = 24;
/**
* Time in hours
*/
@Getter
@Setter
@JSONProperty(required = false)
private volatile int domainsTimeToLive = 48;
/** /**
* Update ASN prefixes every N hours * Update ASN prefixes every N hours
*/ */
@ -62,7 +93,6 @@ public class Config {
* Path where to store temporary data * Path where to store temporary data
*/ */
@Setter @Setter
@Getter @Getter
@JSONProperty @JSONProperty

View File

@ -34,6 +34,11 @@ public class NetworkResourceBundle {
@JSONArrayProperty(type = String.class) @JSONArrayProperty(type = String.class)
private List<String> domains = new ArrayList<>(); private List<String> domains = new ArrayList<>();
@Getter
@Setter
@JSONProperty(required = false)
private boolean resolveDomains = false;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (!(o instanceof NetworkResourceBundle that)) return false; if (!(o instanceof NetworkResourceBundle that)) return false;

View File

@ -12,7 +12,12 @@ import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.core.Util.IPv4Util; import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.*; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -22,19 +27,20 @@ import java.util.concurrent.atomic.AtomicReference;
* Builds the effective set of network resources by combining subscriptions, caches, and filters. * Builds the effective set of network resources by combining subscriptions, caches, and filters.
*/ */
public class NetworkingService extends AppService { public class NetworkingService extends AppService {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final static String CTX = NetworkingService.class.getSimpleName(); private final static String CTX = NetworkingService.class.getSimpleName();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final File cacheFile; private final File domainCacheFile;
private final File asCacheFile;
private final EventListener<NetworkResourceBundle> resourceUpdateSubscription; private final EventListener<NetworkResourceBundle> resourceUpdateSubscription;
private final EventListener<ContextEventsHandler.ConfigChangeContext> configChangeSubscription; private final EventListener<ContextEventsHandler.ConfigChangeContext> configChangeSubscription;
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
@Getter
private final NetworkResourceBundle inputResources = new NetworkResourceBundle();
@Getter
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
private void rebuildInputs() { private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
inputResources.clear(); private final Map<String, ResolverCacheEntry> domainCache = new ConcurrentHashMap<>();
inputResources.add(context.getConfig().getCustomResources());
inputResources.add(context.getServiceManager().getService(SubscriptionService.class).getOutputResources());
triggerUpdate(false);
}
/** /**
* Creates the networking service, wiring subscriptions and restoring cached state. * Creates the networking service, wiring subscriptions and restoring cached state.
@ -51,26 +57,71 @@ public class NetworkingService extends AppService {
NetworkingService.this.rebuildInputs(); NetworkingService.this.rebuildInputs();
} }
}); });
cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json"); domainCacheFile = new File(context.getConfig().getCacheDirectory(), "domain-cache.json");
if (cacheFile.exists() && context.getConfig().isCachingAS()) { asCacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json");
if (asCacheFile.exists() && context.getConfig().isCachingAS()) {
SystemLogger.message("Loading as cache file", CTX); SystemLogger.message("Loading as cache file", CTX);
try (var is = new FileInputStream(cacheFile)) { try (var is = new FileInputStream(asCacheFile)) {
var json = new JSONObject(new JSONTokener(is)); var json = new JSONObject(new JSONTokener(is));
json.keySet().forEach(key -> { json.keySet().forEach(key -> {
var as = Integer.parseInt(key); var as = Integer.parseInt(key);
prefixCache.put(as, JSONUtility.deserializeCollection(json.getJSONArray(key), IPv4Subnet.class, null).toList()); prefixCache.put(as, JSONUtility.deserializeCollection(json.getJSONArray(key), IPv4Subnet.class, null).toList());
}); });
} catch (Exception e) { } catch (Exception e) {
SystemLogger.error("Failed to load as cache file " + cacheFile.getPath(), CTX, e); SystemLogger.error("Failed to load as cache file " + asCacheFile.getPath(), CTX, e);
}
} }
} }
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>(); if (domainCacheFile.exists() && context.getConfig().isCachingAS()) {
@Getter SystemLogger.message("Loading domain cache file", CTX);
private final NetworkResourceBundle inputResources = new NetworkResourceBundle(); try (var is = new FileInputStream(domainCacheFile)) {
@Getter var json = new JSONObject(new JSONTokener(is));
private final NetworkResourceBundle outputResources = new NetworkResourceBundle(); json.keySet().forEach(host -> {
domainCache.put(host, JSONUtility.deserializeStructure(json.getJSONObject(host), ResolverCacheEntry.class));
});
} catch (Exception e) {
SystemLogger.error("Failed to load domain cache file " + asCacheFile.getPath(), CTX, e);
}
}
}
public void performAutoresolve() {
var current = new HashSet<IPv4Subnet>();
domainCache.forEach((host, entry) -> current.addAll(entry.getAddresses().keySet()));
resolveDomains(List.copyOf(context.getServiceManager().getService(SubscriptionService.class).getAutoResolvingDomains()));
var resolved = new HashSet<IPv4Subnet>();
domainCache.forEach((host, entry) -> resolved.addAll(entry.getAddresses().keySet()));
if (resolved.size() != current.size()) {
rebuildInputs();
} else {
var updated = false;
for (var subnet : resolved) {
if (!current.contains(subnet)) {
updated = true;
break;
}
}
if (updated) {
rebuildInputs();
}
}
if(!context.getConfig().isCachingDomains()){
domainCache.clear();
}
}
private void rebuildInputs() {
inputResources.clear();
inputResources.add(context.getConfig().getCustomResources());
inputResources.add(context.getServiceManager().getService(SubscriptionService.class).getOutputResources());
triggerUpdate(false);
}
/** /**
* Indicates whether an update job is currently executing. * Indicates whether an update job is currently executing.
@ -80,8 +131,6 @@ public class NetworkingService extends AppService {
return future != null && !future.isDone() && !future.isCancelled(); return future != null && !future.isDone() && !future.isCancelled();
} }
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
/** /**
* Schedules an update of network resources, optionally ignoring cached prefixes. * Schedules an update of network resources, optionally ignoring cached prefixes.
*/ */
@ -96,50 +145,6 @@ public class NetworkingService extends AppService {
SystemLogger.message("Update is started", CTX); SystemLogger.message("Update is started", CTX);
var config = context.getConfig(); var config = context.getConfig();
var filteredResources = config.getFilteredResources(); var filteredResources = config.getFilteredResources();
var asn = new ArrayList<>(inputResources.getASN());
asn.removeAll(filteredResources.getASN());
var asnToFetch = new ArrayList<>(asn);
if (!ignoreCache) {
asnToFetch.removeAll(prefixCache.keySet());
}
fetchPrefixes(asnToFetch);
if (config.isCachingAS()) {
try (var os = new FileOutputStream(cacheFile)) {
var json = new JSONObject();
prefixCache.forEach((key, asnList) -> {
json.put(String.valueOf(key), JSONUtility.serializeCollection(asnList, IPv4Subnet.class, null));
});
os.write(json.toString().getBytes());
} catch (IOException e) {
SystemLogger.error("Unable to write file " + cacheFile.getPath(), CTX, e);
}
}
var subnets = new HashSet<>(inputResources.getSubnets());
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());
var unmerged = new AtomicInteger();
subnets.forEach(subnet -> {
if (!merged.getMergedSubnets().contains(subnet)) {
unmerged.getAndIncrement();
}
});
SystemLogger.message(subnets.size() + " subnets has been summarized and merged to " + merged.getResult().size() + " new subnets. Unmerged: " + unmerged.get(), CTX);
var domains = new HashSet<>(inputResources.getDomains()); var domains = new HashSet<>(inputResources.getDomains());
filteredResources.getDomains().forEach(domains::remove); filteredResources.getDomains().forEach(domains::remove);
@ -157,6 +162,75 @@ public class NetworkingService extends AppService {
domains.removeAll(domainsToRemove); domains.removeAll(domainsToRemove);
var asn = new ArrayList<>(inputResources.getASN());
asn.removeAll(filteredResources.getASN());
var asnToFetch = new ArrayList<>(asn);
if (!ignoreCache) {
asnToFetch.removeAll(prefixCache.keySet());
}
fetchPrefixes(asnToFetch);
if (config.isCachingAS()) {
try (var os = new FileOutputStream(asCacheFile)) {
var json = new JSONObject();
prefixCache.forEach((key, asnList) -> {
json.put(String.valueOf(key), JSONUtility.serializeCollection(asnList, IPv4Subnet.class, null));
});
os.write(json.toString().getBytes());
} catch (IOException e) {
SystemLogger.error("Unable to write file " + asCacheFile.getPath(), CTX, e);
}
}
resolveDomains(List.copyOf(context.getServiceManager().getService(SubscriptionService.class).getAutoResolvingDomains()));
if (config.isCachingDomains()) {
try (var os = new FileOutputStream(domainCacheFile)) {
var json = new JSONObject();
domainCache.forEach((key, entry) -> {
var serialized = JSONUtility.serializeStructure(entry);
json.put(String.valueOf(key), serialized);
});
os.write(json.toString().getBytes());
} catch (IOException e) {
SystemLogger.error("Unable to write file " + domainCacheFile.getPath(), CTX, e);
}
}
var subnets = new HashSet<>(inputResources.getSubnets());
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);
});
//добавляем отрезолвенные домены
domains.forEach(domain -> {
var entry = domainCache.get(domain);
if (entry != null) {
subnets.addAll(entry.getAddresses().keySet());
}
});
filteredResources.getSubnets().forEach(subnets::remove);
SystemLogger.message("Trying to summary " + subnets.size() + " subnets...", CTX);
var merged = IPv4Util.summarySubnets(subnets, config.getMergeSubnetsWithUsage());
var unmerged = new AtomicInteger();
subnets.forEach(subnet -> {
if (!merged.getMergedSubnets().contains(subnet)) {
unmerged.getAndIncrement();
}
});
SystemLogger.message(subnets.size() + " subnets has been summarized and merged to " + merged.getResult().size() + " new subnets. Unmerged: " + unmerged.get(), CTX);
outputResources.setASN(Collections.unmodifiableList(asn)); outputResources.setASN(Collections.unmodifiableList(asn));
outputResources.setSubnets(merged.getResult()); outputResources.setSubnets(merged.getResult());
outputResources.setDomains(domains.stream().toList()); outputResources.setDomains(domains.stream().toList());
@ -174,6 +248,48 @@ public class NetworkingService extends AppService {
})); }));
} }
private void resolveDomains(List<String> domains) {
var resolvedSubnets = new ArrayList<IPv4Subnet>();
var resolver = context.getServiceManager().getService(ResolverService.class);
for (var domain : domains) {
var task = resolver.resolve(domain);
while (!task.isDone() && !task.isCancelled()) {
Thread.yield();
}
try {
var subnets = task.get();
var entry = domainCache.get(domain);
if(entry == null) {
entry = new ResolverCacheEntry();
domainCache.put(domain, entry);
}
var addresses = entry.getAddresses();
entry.setLastUpdate(Instant.now());
subnets.forEach(subnet -> addresses.put(subnet, Instant.now()));
resolvedSubnets.addAll(domainCache.get(domain).getAddresses().keySet());
} catch (InterruptedException | ExecutionException e) {
SystemLogger.error("Error happened while resolving domain " + domain, CTX, e);
}
}
//remove old entries
for (var domain : domainCache.keySet()) {
var entry = domainCache.get(domain);
var addresses = entry.getAddresses();
for (var subnet : addresses.keySet()) {
var time = addresses.get(subnet);
if (time.isBefore(Instant.now().minus(context.getConfig().getDomainsTimeToLive(), ChronoUnit.HOURS))) {
addresses.remove(subnet);
}
}
if (addresses.isEmpty()) {
domainCache.remove(domain);
}
}
}
/** /**
* Fetches prefixes for the given autonomous systems and stores them in the cache. * Fetches prefixes for the given autonomous systems and stores them in the cache.
*/ */

View File

@ -0,0 +1,23 @@
package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import ru.kirillius.json.JSONMapProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@JSONSerializable
@Getter
@Setter
@NoArgsConstructor
public class ResolverCacheEntry {
@JSONProperty
private Instant lastUpdate = Instant.now();
@JSONMapProperty(keyType = IPv4Subnet.class, valueType = Instant.class)
private Map<IPv4Subnet, Instant> addresses = new HashMap<>();
}

View File

@ -0,0 +1,39 @@
package ru.kirillius.pf.sdn.core.Networking;
import ru.kirillius.pf.sdn.core.AppService;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Util.DomainUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ResolverService extends AppService {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final static String CTX = ResolverService.class.getSimpleName();
public Future<List<IPv4Subnet>> resolve(String host) {
return executor.submit(() -> {
var resolved = new ArrayList<IPv4Subnet>();
for (var domainResolver : context.getConfig().getDomainResolvers()) {
DomainUtil.lookup(host, domainResolver).stream().map(addr -> new IPv4Subnet(addr, 32)).forEach(resolved::add);
}
return resolved;
});
}
public ResolverService(Context context) {
super(context);
}
/**
* Removes event subscriptions and shuts down the executor.
*/
@Override
public void close() throws IOException {
executor.shutdown();
}
}

View File

@ -34,7 +34,6 @@ public class ResourceUpdateService extends AppService {
updateThread.start(); updateThread.start();
} }
/** /**
* Interrupts the update thread and stops scheduling tasks. * Interrupts the update thread and stops scheduling tasks.
*/ */
@ -76,6 +75,11 @@ public class ResourceUpdateService extends AppService {
Wait.when(subscriptionManager::isUpdatingNow); Wait.when(subscriptionManager::isUpdatingNow);
} }
if (uptime % context.getConfig().getDomainLookupInterval() == 0) {
SystemLogger.message("Resolving domains...", CTX);
context.getServiceManager().getService(NetworkingService.class).performAutoresolve();
}
if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) { if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) {
SystemLogger.message("Updating cached AS", CTX); SystemLogger.message("Updating cached AS", CTX);
var networkManager = context.getServiceManager().getService(NetworkingService.class); var networkManager = context.getServiceManager().getService(NetworkingService.class);

View File

@ -8,11 +8,9 @@ import ru.kirillius.utils.logging.SystemLogger;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
/** /**
@ -21,14 +19,14 @@ import java.util.concurrent.atomic.AtomicReference;
public class SubscriptionService extends AppService { public class SubscriptionService extends AppService {
private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>(); private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>(); private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
@Getter @Getter
private final NetworkResourceBundle outputResources = new NetworkResourceBundle(); private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
@Getter
private final List<String> autoResolvingDomains = new CopyOnWriteArrayList<>();
public SubscriptionService(Context context) { public SubscriptionService(Context context) {
super(context); super(context);
@ -45,7 +43,6 @@ public class SubscriptionService extends AppService {
@Getter @Getter
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>(); private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends SubscriptionProvider> T getProvider(Class<T> providerType) { private <T extends SubscriptionProvider> T getProvider(Class<T> providerType) {
if (!providerCache.containsKey(providerType)) { if (!providerCache.containsKey(providerType)) {
@ -62,6 +59,9 @@ public class SubscriptionService extends AppService {
return; return;
} }
updateProcess.set(executor.submit(() -> { updateProcess.set(executor.submit(() -> {
var available = new HashMap<String, NetworkResourceBundle>(); var available = new HashMap<String, NetworkResourceBundle>();
var bundle = new NetworkResourceBundle(); var bundle = new NetworkResourceBundle();
@ -90,6 +90,12 @@ public class SubscriptionService extends AppService {
availableResources.putAll(available); availableResources.putAll(available);
outputResources.clear(); outputResources.clear();
outputResources.add(bundle); outputResources.add(bundle);
available.values().forEach(b -> {
if (b.isResolveDomains()) {
autoResolvingDomains.addAll(b.getDomains());
}
});
try { try {
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources); context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
} catch (Exception e) { } catch (Exception e) {
@ -100,7 +106,6 @@ public class SubscriptionService extends AppService {
private final static String CTX = SubscriptionService.class.getSimpleName(); private final static String CTX = SubscriptionService.class.getSimpleName();
/** /**
* Shuts down the executor used for update tasks. * Shuts down the executor used for update tasks.
*/ */

View File

@ -137,7 +137,7 @@ public class IPv4Util {
var overlapped = new ArrayList<IPv4Subnet>(); var overlapped = new ArrayList<IPv4Subnet>();
var orderedByPrefix = result.stream().sorted(Comparator.comparing(IPv4Subnet::getPrefixLength)).toList(); var orderedByPrefix = result.stream().sorted(Comparator.comparing(IPv4Subnet::getPrefixLength)).toList();
orderedByPrefix.stream() orderedByPrefix.stream()
.filter(subnet -> subnet.getPrefixLength() < 32) .filter(subnet -> subnet.getPrefixLength() > 32)
.forEach(parent -> orderedByPrefix.forEach(subnet -> { .forEach(parent -> orderedByPrefix.forEach(subnet -> {
if (subnet.equals(parent)) { if (subnet.equals(parent)) {
return; return;

View File

@ -0,0 +1,15 @@
package ru.kirillius.pf.sdn.core.Networking;
import org.junit.jupiter.api.Test;
import java.time.Instant;
class ResBundleTest {
@Test
void testInit() {
var b = new ResolverCacheEntry();
b.getAddresses().put(new IPv4Subnet("127.0.0.1/32"), Instant.now());
}
}

View File

@ -31,6 +31,27 @@ class IPv4UtilTest {
//subnets.forEach(System.out::println);
var merged = IPv4Util.summarySubnets(subnets, 51).getResult();
merged.forEach(System.out::println);
assertThat(merged).isNotNull();
}
@Test
void summarySubnetsShishanyaCase() {
var subnets = new ArrayList<IPv4Subnet>();
subnets.add(new IPv4Subnet("8.6.112.0/32"));
subnets.add(new IPv4Subnet("8.6.112.0/24"));
//subnets.forEach(System.out::println); //subnets.forEach(System.out::println);
var merged = IPv4Util.summarySubnets(subnets, 51).getResult(); var merged = IPv4Util.summarySubnets(subnets, 51).getResult();

View File

@ -14,6 +14,7 @@ const FIELD_IDS = {
domainsInput: 'local-storages-domains', domainsInput: 'local-storages-domains',
asnInput: 'local-storages-asn', asnInput: 'local-storages-asn',
subnetsInput: 'local-storages-subnets', subnetsInput: 'local-storages-subnets',
resolveDomainsCheckbox: 'local-storages-resolve-domains',
modalStatus: 'local-storages-modal-status', modalStatus: 'local-storages-modal-status',
modalSaveButton: 'local-storages-save-btn', modalSaveButton: 'local-storages-save-btn',
modalCancelButton: 'local-storages-cancel-btn' modalCancelButton: 'local-storages-cancel-btn'
@ -253,6 +254,7 @@ function openModal(mode, resourceName = '') {
const $domainsInput = $(SELECTORS.domainsInput); const $domainsInput = $(SELECTORS.domainsInput);
const $asnInput = $(SELECTORS.asnInput); const $asnInput = $(SELECTORS.asnInput);
const $subnetsInput = $(SELECTORS.subnetsInput); const $subnetsInput = $(SELECTORS.subnetsInput);
const $resolveDomainsCheckbox = $(SELECTORS.resolveDomainsCheckbox);
const $saveButton = $(SELECTORS.modalSaveButton); const $saveButton = $(SELECTORS.modalSaveButton);
setModalStatus('', ''); setModalStatus('', '');
@ -266,6 +268,7 @@ function openModal(mode, resourceName = '') {
$domainsInput.val(joinLines(resource.domains)); $domainsInput.val(joinLines(resource.domains));
$asnInput.val(joinLines(resource.ASN)); $asnInput.val(joinLines(resource.ASN));
$subnetsInput.val(joinLines(resource.subnets)); $subnetsInput.val(joinLines(resource.subnets));
$resolveDomainsCheckbox.prop('checked', resource.resolveDomains === true);
} else { } else {
$title.text('Создание ресурса'); $title.text('Создание ресурса');
$nameInput.val('').prop('disabled', false); $nameInput.val('').prop('disabled', false);
@ -273,6 +276,7 @@ function openModal(mode, resourceName = '') {
$domainsInput.val(''); $domainsInput.val('');
$asnInput.val(''); $asnInput.val('');
$subnetsInput.val(''); $subnetsInput.val('');
$resolveDomainsCheckbox.prop('checked', false);
} }
$saveButton.prop('disabled', false).text('Сохранить'); $saveButton.prop('disabled', false).text('Сохранить');
@ -314,6 +318,7 @@ async function handleModalSubmit(event) {
const $domainsInput = $(SELECTORS.domainsInput); const $domainsInput = $(SELECTORS.domainsInput);
const $asnInput = $(SELECTORS.asnInput); const $asnInput = $(SELECTORS.asnInput);
const $subnetsInput = $(SELECTORS.subnetsInput); const $subnetsInput = $(SELECTORS.subnetsInput);
const $resolveDomainsCheckbox = $(SELECTORS.resolveDomainsCheckbox);
const nameValue = ($nameInput.val() || '').trim(); const nameValue = ($nameInput.val() || '').trim();
const validationError = validateName(nameValue); const validationError = validateName(nameValue);
@ -350,6 +355,7 @@ async function handleModalSubmit(event) {
parsedAsn.value, parsedAsn.value,
subnetsValue, subnetsValue,
[], [],
$resolveDomainsCheckbox.is(':checked'),
selectedRepository, selectedRepository,
false false
); );
@ -509,6 +515,12 @@ export const LocalStoragesPage = {
<label for="${FIELD_IDS.subnetsInput}">Подсети (каждая с новой строки)</label> <label for="${FIELD_IDS.subnetsInput}">Подсети (каждая с новой строки)</label>
<textarea id="${FIELD_IDS.subnetsInput}" class="form-control" rows="4" placeholder="192.0.2.0/24"></textarea> <textarea id="${FIELD_IDS.subnetsInput}" class="form-control" rows="4" placeholder="192.0.2.0/24"></textarea>
</div> </div>
<div class="form-group-checkbox" style="margin-bottom: 0;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="${FIELD_IDS.resolveDomainsCheckbox}">
Автоматически получать IP адреса доменов
</label>
</div>
<div id="${FIELD_IDS.modalStatus}" class="error-message" style="display: none;"></div> <div id="${FIELD_IDS.modalStatus}" class="error-message" style="display: none;"></div>
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 8px;"> <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 8px;">
<button type="button" id="${FIELD_IDS.modalCancelButton}" class="btn-secondary" style="padding: 10px 18px;">Отмена</button> <button type="button" id="${FIELD_IDS.modalCancelButton}" class="btn-secondary" style="padding: 10px 18px;">Отмена</button>

View File

@ -22,7 +22,11 @@ const FIELD_IDS = {
customDomains: 'settings-custom-domains', customDomains: 'settings-custom-domains',
filteredASN: 'settings-filtered-asn', filteredASN: 'settings-filtered-asn',
filteredSubnets: 'settings-filtered-subnets', filteredSubnets: 'settings-filtered-subnets',
filteredDomains: 'settings-filtered-domains' filteredDomains: 'settings-filtered-domains',
cachingDomains: 'settings-caching-domains',
domainResolvers: 'settings-domain-resolvers',
domainLookupInterval: 'settings-domain-lookup-interval',
domainsTimeToLive: 'settings-domains-time-to-live'
}; };
const CLASS_NAMES = { const CLASS_NAMES = {
@ -210,6 +214,10 @@ function renderSettingsForm() {
const mergeSubnets = !!getConfigValue('mergeSubnets', false); const mergeSubnets = !!getConfigValue('mergeSubnets', false);
const displayDebuggingInfo = !!getConfigValue('displayDebuggingInfo', false); const displayDebuggingInfo = !!getConfigValue('displayDebuggingInfo', false);
const mergeSubnetsWithUsage = getConfigValue('mergeSubnetsWithUsage', 80); const mergeSubnetsWithUsage = getConfigValue('mergeSubnetsWithUsage', 80);
const cachingDomains = !!getConfigValue('cachingDomains', false);
const domainResolvers = getConfigValue('domainResolvers', []);
const domainLookupInterval = getConfigValue('domainLookupInterval', 60);
const domainsTimeToLive = getConfigValue('domainsTimeToLive', 24);
const customResources = getConfigValue('customResources', {}); const customResources = getConfigValue('customResources', {});
const filteredResources = getConfigValue('filteredResources', {}); const filteredResources = getConfigValue('filteredResources', {});
@ -261,6 +269,24 @@ function renderSettingsForm() {
<label for="${FIELD_IDS.mergeSubnetsWithUsage}">Объединять подсети с заполнением &gt;= %</label> <label for="${FIELD_IDS.mergeSubnetsWithUsage}">Объединять подсети с заполнением &gt;= %</label>
<input type="number" min="51" max="99" id="${FIELD_IDS.mergeSubnetsWithUsage}" class="form-control" value="${mergeSubnetsWithUsage}"> <input type="number" min="51" max="99" id="${FIELD_IDS.mergeSubnetsWithUsage}" class="form-control" value="${mergeSubnetsWithUsage}">
</div> </div>
<h3 class="config-section-title" style="margin-top: 40px;">DNS Resolver</h3>
<div class="form-group checkbox-group">
<input type="checkbox" id="${FIELD_IDS.cachingDomains}" ${cachingDomains ? 'checked' : ''}>
<label for="${FIELD_IDS.cachingDomains}" style="margin-bottom: 0;">Кешировать домены</label>
</div>
<div class="form-group">
<label for="${FIELD_IDS.domainResolvers}">DNS серверы</label>
<textarea id="${FIELD_IDS.domainResolvers}" class="form-control" rows="3" placeholder="8.8.8.8">${textareaFromArray(domainResolvers)}</textarea>
</div>
<div class="form-group">
<label for="${FIELD_IDS.domainLookupInterval}">Интервал проверки доменов (минуты): <span id="domain-lookup-value">${domainLookupInterval}</span></label>
<input type="range" min="1" max="${60 * 24}" id="${FIELD_IDS.domainLookupInterval}" class="form-control" value="${domainLookupInterval}" style="padding: 0;">
</div>
<div class="form-group">
<label for="${FIELD_IDS.domainsTimeToLive}">Время жизни кешированных доменов (часы): <span id="domains-ttl-value">${domainsTimeToLive}</span></label>
<input type="range" min="1" max="${24 * 7}" id="${FIELD_IDS.domainsTimeToLive}" class="form-control" value="${domainsTimeToLive}" style="padding: 0;">
</div>
<div style="margin-top: 30px;"> <div style="margin-top: 30px;">
<h4 style="margin-bottom: 15px;">Дополнительные ресурсы</h4> <h4 style="margin-bottom: 15px;">Дополнительные ресурсы</h4>
<div class="form-group"> <div class="form-group">
@ -384,6 +410,8 @@ function collectSettingsFromForm() {
const updateSubscriptionsInterval = parseNumberInRange($(`#${FIELD_IDS.updateSubscriptionsInterval}`).val(), 1, 24, 'Интервал обновления подписок'); const updateSubscriptionsInterval = parseNumberInRange($(`#${FIELD_IDS.updateSubscriptionsInterval}`).val(), 1, 24, 'Интервал обновления подписок');
const updateASInterval = parseNumberInRange($(`#${FIELD_IDS.updateASInterval}`).val(), 1, 24, 'Интервал обновления ASN'); const updateASInterval = parseNumberInRange($(`#${FIELD_IDS.updateASInterval}`).val(), 1, 24, 'Интервал обновления ASN');
const mergeSubnetsWithUsage = parseNumberInRange($(`#${FIELD_IDS.mergeSubnetsWithUsage}`).val(), 51, 99, 'Процент объединения подсетей'); const mergeSubnetsWithUsage = parseNumberInRange($(`#${FIELD_IDS.mergeSubnetsWithUsage}`).val(), 51, 99, 'Процент объединения подсетей');
const domainLookupInterval = parseNumberInRange($(`#${FIELD_IDS.domainLookupInterval}`).val(), 1, 60 * 24, 'Интервал проверки доменов');
const domainsTimeToLive = parseNumberInRange($(`#${FIELD_IDS.domainsTimeToLive}`).val(), 1, 24 * 7, 'Время жизни кешированных доменов');
const httpPortValue = parseInt($(`#${FIELD_IDS.httpPort}`).val(), 10); const httpPortValue = parseInt($(`#${FIELD_IDS.httpPort}`).val(), 10);
if (Number.isNaN(httpPortValue) || httpPortValue < 1 || httpPortValue > 65535) { if (Number.isNaN(httpPortValue) || httpPortValue < 1 || httpPortValue > 65535) {
@ -413,6 +441,10 @@ function collectSettingsFromForm() {
mergeSubnets: $(`#${FIELD_IDS.mergeSubnets}`).prop('checked'), mergeSubnets: $(`#${FIELD_IDS.mergeSubnets}`).prop('checked'),
displayDebuggingInfo: $(`#${FIELD_IDS.displayDebuggingInfo}`).prop('checked'), displayDebuggingInfo: $(`#${FIELD_IDS.displayDebuggingInfo}`).prop('checked'),
mergeSubnetsWithUsage, mergeSubnetsWithUsage,
cachingDomains: $(`#${FIELD_IDS.cachingDomains}`).prop('checked'),
domainResolvers: parseTextAreaLines($(`#${FIELD_IDS.domainResolvers}`)),
domainLookupInterval,
domainsTimeToLive,
subscriptions, subscriptions,
customResources, customResources,
filteredResources filteredResources
@ -468,6 +500,12 @@ function attachEventHandlers() {
$(this).closest(`.${CLASS_NAMES.subscriptionEntry}`).remove(); $(this).closest(`.${CLASS_NAMES.subscriptionEntry}`).remove();
}); });
$(`#${FIELD_IDS.changePasswordButton}`).off('click').on('click', handleChangePassword); $(`#${FIELD_IDS.changePasswordButton}`).off('click').on('click', handleChangePassword);
$(`#${FIELD_IDS.domainLookupInterval}`).off('input').on('input', function () {
$('#domain-lookup-value').text($(this).val());
});
$(`#${FIELD_IDS.domainsTimeToLive}`).off('input').on('input', function () {
$('#domains-ttl-value').text($(this).val());
});
} }
function detachEventHandlers() { function detachEventHandlers() {
@ -475,6 +513,8 @@ function detachEventHandlers() {
$(`#${FIELD_IDS.addSubscriptionButton}`).off('click'); $(`#${FIELD_IDS.addSubscriptionButton}`).off('click');
$(`#${FIELD_IDS.subscriptionsList}`).off('click', `.${CLASS_NAMES.subscriptionRemove}`); $(`#${FIELD_IDS.subscriptionsList}`).off('click', `.${CLASS_NAMES.subscriptionRemove}`);
$(`#${FIELD_IDS.changePasswordButton}`).off('click'); $(`#${FIELD_IDS.changePasswordButton}`).off('click');
$(`#${FIELD_IDS.domainLookupInterval}`).off('input');
$(`#${FIELD_IDS.domainsTimeToLive}`).off('input');
} }
export const SettingsPage = { export const SettingsPage = {