From ee3f4f3fe58dadaf45118149274c210f4ed7525a Mon Sep 17 00:00:00 2001 From: kirillius Date: Sun, 17 May 2026 21:08:23 +0300 Subject: [PATCH] =?UTF-8?q?WIP=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20resolver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/ru/kirillius/pf/sdn/App.java | 3 +- .../pf/sdn/web/RPC/SubscriptionManager.java | 6 +- .../java/ru/kirillius/pf/sdn/core/Config.java | 8 +- .../core/Networking/NetworkingService.java | 17 ++- .../core/Networking/ResolverCacheEntry.java | 5 +- .../Networking/ResolverServiceObsolete.java | 120 ------------------ .../pf/sdn/core/ResourceUpdateService.java | 6 +- .../pf/sdn/core/Networking/ResBundleTest.java | 15 +++ webui/src/pages/LocalStorages.js | 12 ++ webui/src/pages/Settings.js | 42 +++++- 10 files changed, 99 insertions(+), 135 deletions(-) delete mode 100644 core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverServiceObsolete.java create mode 100644 core/src/test/java/ru/kirillius/pf/sdn/core/Networking/ResBundleTest.java 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 0b81991..5e6d507 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/App.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java @@ -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.Networking.BGPInfoService; 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.SubscriptionService; 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. */ 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); infoService.addProvider(new HEInfoProvider()); infoService.addProvider(new RIPEInfoProvider()); diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java index 73b01d4..136b35e 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java @@ -87,6 +87,7 @@ public class SubscriptionManager implements RPC { @JRPCArgument(name = "ASN") JSONArray ASN, @JRPCArgument(name = "subnets") JSONArray subnets, @JRPCArgument(name = "addresses") JSONArray addresses, + @JRPCArgument(name = "autoResolve") boolean autoResolve, @JRPCArgument(name = "storage") String storage, @JRPCArgument(name = "subscribe") boolean subscribe) throws IOException { var repositoryConfig = findLocalRepo(storage); @@ -97,7 +98,7 @@ public class SubscriptionManager implements RPC { var domainList = new ArrayList(); - domains.forEach(d->domainList.add(d.toString())); + domains.forEach(d -> domainList.add(d.toString())); try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(directory, name + ".json"))))) { var merged = JSONUtility.deserializeCollection(subnets, IPv4Subnet.class, null).collect(Collectors.toList()); @@ -113,6 +114,7 @@ public class SubscriptionManager implements RPC { .subnets(merged) .domains(domainList) .description(description) + .resolveDomains(autoResolve) .build() ).toString(2)); } @@ -124,7 +126,6 @@ public class SubscriptionManager implements RPC { context.getServiceManager().getService(SubscriptionService.class).triggerUpdate(); } - 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(); if (optional.isEmpty()) { @@ -134,7 +135,6 @@ public class SubscriptionManager implements RPC { return optional.get(); } - @JRPCMethod @ProtectedMethod public void removeLocalResourceFile( 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 5ca15fc..52fec99 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 @@ -58,10 +58,10 @@ public class Config { @JSONProperty(required = false) private volatile int domainLookupInterval = 5; - @Getter - @Setter - @JSONProperty(required = false) - private volatile int autoLookupPrefixLength = 24; +// @Getter +// @Setter +// @JSONProperty(required = false) +// private volatile int autoLookupPrefixLength = 24; /** * Time in hours diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java index 77a7592..fa1eb73 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java @@ -84,10 +84,10 @@ public class NetworkingService extends AppService { } } - executor.execute(this::autoResolverWorker); + } - private void autoResolverWorker() { + public void performAutoresolve() { var current = new HashSet(); domainCache.forEach((host, entry) -> current.addAll(entry.getAddresses().keySet())); @@ -110,6 +110,10 @@ public class NetworkingService extends AppService { rebuildInputs(); } } + + if(!context.getConfig().isCachingDomains()){ + domainCache.clear(); + } } private void rebuildInputs() { @@ -186,7 +190,8 @@ public class NetworkingService extends AppService { try (var os = new FileOutputStream(domainCacheFile)) { var json = new JSONObject(); domainCache.forEach((key, entry) -> { - json.put(String.valueOf(key), JSONUtility.serializeStructure(domainCache.get(key))); + var serialized = JSONUtility.serializeStructure(entry); + json.put(String.valueOf(key), serialized); }); os.write(json.toString().getBytes()); } catch (IOException e) { @@ -254,6 +259,12 @@ public class NetworkingService extends AppService { 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())); diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverCacheEntry.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverCacheEntry.java index 60bfd21..4dea486 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverCacheEntry.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverCacheEntry.java @@ -8,6 +8,7 @@ import ru.kirillius.json.JSONProperty; import ru.kirillius.json.JSONSerializable; import java.time.Instant; +import java.util.HashMap; import java.util.Map; @JSONSerializable @@ -16,7 +17,7 @@ import java.util.Map; @NoArgsConstructor public class ResolverCacheEntry { @JSONProperty - private Instant lastUpdate; + private Instant lastUpdate = Instant.now(); @JSONMapProperty(keyType = IPv4Subnet.class, valueType = Instant.class) - private Map addresses; + private Map addresses = new HashMap<>(); } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverServiceObsolete.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverServiceObsolete.java deleted file mode 100644 index 74b0e6f..0000000 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ResolverServiceObsolete.java +++ /dev/null @@ -1,120 +0,0 @@ -package ru.kirillius.pf.sdn.core.Networking; - -import lombok.Getter; -import org.json.JSONObject; -import org.json.JSONTokener; -import ru.kirillius.json.JSONUtility; -import ru.kirillius.pf.sdn.core.AppService; -import ru.kirillius.pf.sdn.core.Context; -import ru.kirillius.pf.sdn.core.Util.DomainUtil; -import ru.kirillius.utils.logging.SystemLogger; - -import java.io.*; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.*; - - -public class ResolverServiceObsolete extends AppService { - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private final static String CTX = ResolverServiceObsolete.class.getSimpleName(); - - private final File cacheFile; - - private final Map cacheEntries = new ConcurrentHashMap<>(); - private final Future backgroundWorker; - - public void saveCache() throws IOException { - try (var writer = new BufferedWriter(new FileWriter(cacheFile))) { - writer.write(JSONUtility.serializeMap(cacheEntries, String.class, ResolverCacheEntry.class, null, null).toString()); - } - } - - public void maintainCache() { - cacheEntries.keySet().forEach(host -> { - var entry = cacheEntries.get(host); - var lastSeen = entry.getLastSeen(); - for (var subnet : lastSeen.keySet()) { - if (lastSeen.get(subnet).isBefore(Instant.now().minus(context.getConfig().getDomainsTimeToLive(), ChronoUnit.HOURS))) { - lastSeen.remove(subnet); - } - } - }); - } - - public Future> resolve(String host, boolean useCache) { - return executor.submit(() -> { - if (useCache && cacheEntries.containsKey(host)) { - var cached = cacheEntries.get(host); - if (cached.getLastUpdate().isAfter(Instant.now().minus(context.getConfig().getDomainLookupInterval(), ChronoUnit.MINUTES))) { - return cached.getAddresses(); - } - } - var resolved = new HashSet(); - for (var domainResolver : context.getConfig().getDomainResolvers()) { - DomainUtil.lookup(host, domainResolver).stream().map(addr -> new IPv4Subnet(addr, 32)).forEach(resolved::add); - } - - if (useCache) { - if (!cacheEntries.containsKey(host)) { - cacheEntries.put(host, new ResolverCacheEntry()); - } - var cached = cacheEntries.get(host); - cached.setLastUpdate(Instant.now()); - cached.getAddresses().addAll(resolved); - var lastSeen = cached.getLastSeen(); - resolved.forEach(net -> lastSeen.put(net, Instant.now())); - resolved.addAll(cached.getAddresses()); - } - - return resolved.stream().toList(); - }); - } - - /** - * Creates the networking service, wiring subscriptions and restoring cached state. - */ - public ResolverServiceObsolete(Context context) { - super(context); - - cacheFile = new File(context.getConfig().getCacheDirectory(), "resolver-cache.json"); - if (cacheFile.exists() && context.getConfig().isCachingAS()) { - SystemLogger.message("Loading resolver cache file", CTX); - try (var is = new FileInputStream(cacheFile)) { - var json = new JSONObject(new JSONTokener(is)); - var deserialized = JSONUtility.deserializeMap(json, String.class, ResolverCacheEntry.class, null, null); - cacheEntries.putAll(deserialized); - } catch (Exception e) { - SystemLogger.error("Failed to load resolver cache file " + cacheFile.getPath(), CTX, e); - } - } - - backgroundWorker = executor.submit(this::autoResolve); - } - - @Getter - private final List autoLookupHosts = new CopyOnWriteArrayList<>(); - - private void autoResolve() { - try { - Thread.sleep(context.getConfig().getDomainLookupInterval()); - autoLookupHosts.stream().toList().forEach(host -> { - //TODO продумать обновление - }); - } catch (InterruptedException e) { - return; - } - } - - /** - * Removes event subscriptions and shuts down the executor. - */ - @Override - public void close() throws IOException { - backgroundWorker.cancel(true); - executor.shutdown(); - } -} diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java index 5e19bbf..141e5f8 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java @@ -34,7 +34,6 @@ public class ResourceUpdateService extends AppService { updateThread.start(); } - /** * Interrupts the update thread and stops scheduling tasks. */ @@ -76,6 +75,11 @@ public class ResourceUpdateService extends AppService { 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) { SystemLogger.message("Updating cached AS", CTX); var networkManager = context.getServiceManager().getService(NetworkingService.class); diff --git a/core/src/test/java/ru/kirillius/pf/sdn/core/Networking/ResBundleTest.java b/core/src/test/java/ru/kirillius/pf/sdn/core/Networking/ResBundleTest.java new file mode 100644 index 0000000..f43e5eb --- /dev/null +++ b/core/src/test/java/ru/kirillius/pf/sdn/core/Networking/ResBundleTest.java @@ -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()); + + } +} \ No newline at end of file diff --git a/webui/src/pages/LocalStorages.js b/webui/src/pages/LocalStorages.js index 7ca4a6a..efdd108 100644 --- a/webui/src/pages/LocalStorages.js +++ b/webui/src/pages/LocalStorages.js @@ -14,6 +14,7 @@ const FIELD_IDS = { domainsInput: 'local-storages-domains', asnInput: 'local-storages-asn', subnetsInput: 'local-storages-subnets', + resolveDomainsCheckbox: 'local-storages-resolve-domains', modalStatus: 'local-storages-modal-status', modalSaveButton: 'local-storages-save-btn', modalCancelButton: 'local-storages-cancel-btn' @@ -253,6 +254,7 @@ function openModal(mode, resourceName = '') { const $domainsInput = $(SELECTORS.domainsInput); const $asnInput = $(SELECTORS.asnInput); const $subnetsInput = $(SELECTORS.subnetsInput); + const $resolveDomainsCheckbox = $(SELECTORS.resolveDomainsCheckbox); const $saveButton = $(SELECTORS.modalSaveButton); setModalStatus('', ''); @@ -266,6 +268,7 @@ function openModal(mode, resourceName = '') { $domainsInput.val(joinLines(resource.domains)); $asnInput.val(joinLines(resource.ASN)); $subnetsInput.val(joinLines(resource.subnets)); + $resolveDomainsCheckbox.prop('checked', resource.resolveDomains === true); } else { $title.text('Создание ресурса'); $nameInput.val('').prop('disabled', false); @@ -273,6 +276,7 @@ function openModal(mode, resourceName = '') { $domainsInput.val(''); $asnInput.val(''); $subnetsInput.val(''); + $resolveDomainsCheckbox.prop('checked', false); } $saveButton.prop('disabled', false).text('Сохранить'); @@ -314,6 +318,7 @@ async function handleModalSubmit(event) { const $domainsInput = $(SELECTORS.domainsInput); const $asnInput = $(SELECTORS.asnInput); const $subnetsInput = $(SELECTORS.subnetsInput); + const $resolveDomainsCheckbox = $(SELECTORS.resolveDomainsCheckbox); const nameValue = ($nameInput.val() || '').trim(); const validationError = validateName(nameValue); @@ -350,6 +355,7 @@ async function handleModalSubmit(event) { parsedAsn.value, subnetsValue, [], + $resolveDomainsCheckbox.is(':checked'), selectedRepository, false ); @@ -509,6 +515,12 @@ export const LocalStoragesPage = { +
+ +
diff --git a/webui/src/pages/Settings.js b/webui/src/pages/Settings.js index 741e505..5117c6f 100644 --- a/webui/src/pages/Settings.js +++ b/webui/src/pages/Settings.js @@ -22,7 +22,11 @@ const FIELD_IDS = { customDomains: 'settings-custom-domains', filteredASN: 'settings-filtered-asn', 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 = { @@ -210,6 +214,10 @@ function renderSettingsForm() { const mergeSubnets = !!getConfigValue('mergeSubnets', false); const displayDebuggingInfo = !!getConfigValue('displayDebuggingInfo', false); 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 filteredResources = getConfigValue('filteredResources', {}); @@ -261,6 +269,24 @@ function renderSettingsForm() {
+ +

DNS Resolver

+
+ + +
+
+ + +
+
+ + +
+
+ + +

Дополнительные ресурсы

@@ -384,6 +410,8 @@ function collectSettingsFromForm() { const updateSubscriptionsInterval = parseNumberInRange($(`#${FIELD_IDS.updateSubscriptionsInterval}`).val(), 1, 24, 'Интервал обновления подписок'); const updateASInterval = parseNumberInRange($(`#${FIELD_IDS.updateASInterval}`).val(), 1, 24, 'Интервал обновления ASN'); 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); if (Number.isNaN(httpPortValue) || httpPortValue < 1 || httpPortValue > 65535) { @@ -413,6 +441,10 @@ function collectSettingsFromForm() { mergeSubnets: $(`#${FIELD_IDS.mergeSubnets}`).prop('checked'), displayDebuggingInfo: $(`#${FIELD_IDS.displayDebuggingInfo}`).prop('checked'), mergeSubnetsWithUsage, + cachingDomains: $(`#${FIELD_IDS.cachingDomains}`).prop('checked'), + domainResolvers: parseTextAreaLines($(`#${FIELD_IDS.domainResolvers}`)), + domainLookupInterval, + domainsTimeToLive, subscriptions, customResources, filteredResources @@ -468,6 +500,12 @@ function attachEventHandlers() { $(this).closest(`.${CLASS_NAMES.subscriptionEntry}`).remove(); }); $(`#${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() { @@ -475,6 +513,8 @@ function detachEventHandlers() { $(`#${FIELD_IDS.addSubscriptionButton}`).off('click'); $(`#${FIELD_IDS.subscriptionsList}`).off('click', `.${CLASS_NAMES.subscriptionRemove}`); $(`#${FIELD_IDS.changePasswordButton}`).off('click'); + $(`#${FIELD_IDS.domainLookupInterval}`).off('input'); + $(`#${FIELD_IDS.domainsTimeToLive}`).off('input'); } export const SettingsPage = {