From 6a2f51ce2ad7c54860bc4b9df9e66ad74a8669eb Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:11:39 +0200 Subject: [PATCH 01/10] feat: probe and add local gateway if available --- src/lib/local-gateway.ts | 32 ++++++++++++++++++++++++++++++++ src/pages/config.tsx | 25 +++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/lib/local-gateway.ts diff --git a/src/lib/local-gateway.ts b/src/lib/local-gateway.ts new file mode 100644 index 00000000..815a0175 --- /dev/null +++ b/src/lib/local-gateway.ts @@ -0,0 +1,32 @@ +import { uiLogger } from './logger' + +export const localGwUrl = 'http://127.0.0.1:8080' +const localGwTestUrl = `${localGwUrl}/ipfs/bafkqablimvwgy3y?format=raw` +const expectedContentType = 'application/vnd.ipld.raw' +const expectedResponseBody = 'hello' + +const log = uiLogger.forComponent('local-gateway-prober') + +export async function hasLocalGateway (): Promise { + try { + log(`probing for local trustless gateway at ${localGwTestUrl}`) + const resp = await fetch(localGwTestUrl) + if (!resp.ok) { + return false + } + if (resp.headers.get('Content-Type') !== expectedContentType) { + return false + } + const respBody = await resp.text() + + if (respBody === expectedResponseBody) { + log(`found local trustless gateway at ${localGwTestUrl}`) + return true + } else { + return false + } + } catch (e: unknown) { + log.error('failed to probe trustless gateway', e) + return false + } +} diff --git a/src/pages/config.tsx b/src/pages/config.tsx index 4ffa1340..44a53255 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -8,6 +8,7 @@ import { RouteContext } from '../context/router-context.jsx' import { ServiceWorkerProvider } from '../context/service-worker-context.jsx' import { HeliaServiceWorkerCommsChannel } from '../lib/channel.js' import { defaultDnsJsonResolvers, defaultGateways, defaultRouters, getConfig, loadConfigFromLocalStorage, resetConfig } from '../lib/config-db.js' +import { hasLocalGateway, localGwUrl } from '../lib/local-gateway.js' import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js' import { getUiComponentLogger, uiLogger } from '../lib/logger.js' import './default-page-styles.css' @@ -84,10 +85,30 @@ function ConfigPage (): React.JSX.Element | null { window.parent?.postMessage({ source: 'helia-sw-config-iframe', target: 'PARENT', action: 'RELOAD_CONFIG', config }, { targetOrigin }) - log.trace('config-page: RELOAD_CONFIG sent to parent window') + log.trace('RELOAD_CONFIG sent to parent window') }, []) useEffect(() => { + hasLocalGateway() + .then(async hasLocalGw => { + if (hasLocalGw) { + // check if local storage has it. + const unparsedGwConf = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) + const gwConf = unparsedGwConf != null ? JSON.parse(unparsedGwConf) as string[] : defaultGateways + if (!gwConf.includes(localGwUrl)) { + log(`Adding ${localGwUrl} to gateway list`) + gwConf.push(localGwUrl) + } + // Update localStorage + localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(gwConf)) + await loadConfigFromLocalStorage() + await channel.messageAndWaitForResponse('SW', { target: 'SW', action: 'RELOAD_CONFIG' }) + await postFromIframeToParentSw() + setResetKey((prev) => prev + 1) + } + }).catch(err => { + log.error('failed to probe for local gateway', err) + }) /** * On initial load, we want to send the config to the parent window, so that the reload page can auto-reload if enabled, and the subdomain registered service worker gets the latest config without user interaction. */ @@ -120,7 +141,7 @@ function ConfigPage (): React.JSX.Element | null { }, []) return ( -
+
From d08fa32e8d4612f3edaf8b11864c7639b515422c Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:49:08 +0200 Subject: [PATCH 02/10] fix: indentation --- src/pages/config.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/config.tsx b/src/pages/config.tsx index 44a53255..c0c0a051 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -141,7 +141,7 @@ function ConfigPage (): React.JSX.Element | null { }, []) return ( -
+
From 8132e8d8b8f664a02834c7a235b1773232979a14 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:58:43 +0200 Subject: [PATCH 03/10] fix: import --- src/lib/local-gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/local-gateway.ts b/src/lib/local-gateway.ts index 815a0175..d44d4ede 100644 --- a/src/lib/local-gateway.ts +++ b/src/lib/local-gateway.ts @@ -1,4 +1,4 @@ -import { uiLogger } from './logger' +import { uiLogger } from './logger.js' export const localGwUrl = 'http://127.0.0.1:8080' const localGwTestUrl = `${localGwUrl}/ipfs/bafkqablimvwgy3y?format=raw` From 76f32a8c4504ed5fffb2dd5cbc96abbb593add8c Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:06:00 +0200 Subject: [PATCH 04/10] feat: make routers optional --- src/lib/config-db.ts | 4 ++-- src/lib/local-gateway.ts | 1 + src/pages/config.tsx | 24 +++++++++++++++++++++--- src/sw.ts | 6 ++++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/lib/config-db.ts b/src/lib/config-db.ts index a858c359..aa9f4a93 100644 --- a/src/lib/config-db.ts +++ b/src/lib/config-db.ts @@ -12,7 +12,7 @@ export interface ConfigDb extends BaseDbConfig { } export const defaultGateways = ['https://trustless-gateway.link'] -export const defaultRouters = ['https://delegated-ipfs.dev'] +export const defaultRouters = [] export const defaultDnsJsonResolvers = { '.': 'https://delegated-ipfs.dev/dns-query' } @@ -100,7 +100,7 @@ export async function getConfig (logger: ComponentLogger): Promise { gateways = [...defaultGateways] } - if (routers == null || routers.length === 0) { + if (routers == null) { routers = [...defaultRouters] } if (dnsJsonResolvers == null || Object.keys(dnsJsonResolvers).length === 0) { diff --git a/src/lib/local-gateway.ts b/src/lib/local-gateway.ts index d44d4ede..a8eb4601 100644 --- a/src/lib/local-gateway.ts +++ b/src/lib/local-gateway.ts @@ -1,6 +1,7 @@ import { uiLogger } from './logger.js' export const localGwUrl = 'http://127.0.0.1:8080' +// export const localGwUrl = 'http://localhost:8080' const localGwTestUrl = `${localGwUrl}/ipfs/bafkqablimvwgy3y?format=raw` const expectedContentType = 'application/vnd.ipld.raw' const expectedResponseBody = 'hello' diff --git a/src/pages/config.tsx b/src/pages/config.tsx index c0c0a051..400f4e70 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -17,7 +17,7 @@ const uiComponentLogger = getUiComponentLogger('config-page') const log = uiLogger.forComponent('config-page') const channel = new HeliaServiceWorkerCommsChannel('WINDOW', uiComponentLogger) -const urlValidationFn = (value: string): Error | null => { +const gatewayArrayValidationFn = (value: string): Error | null => { try { const urls = JSON.parse(value) satisfies string[] let i = 0 @@ -38,6 +38,24 @@ const urlValidationFn = (value: string): Error | null => { } } +const routersArrayValidationFn = (value: string): Error | null => { + try { + const urls = JSON.parse(value) satisfies string[] + let i = 0 + try { + urls.map((url, index) => { + i = index + return new URL(url) + }) + } catch (e) { + throw new Error(`URL "${urls[i]}" at index ${i} is not valid`) + } + return null + } catch (err) { + return err as Error + } +} + const dnsJsonValidationFn = (value: string): Error | null => { try { const urls: Record = JSON.parse(value) @@ -143,8 +161,8 @@ function ConfigPage (): React.JSX.Element | null { return (
- - + + diff --git a/src/sw.ts b/src/sw.ts index 019c3e66..f461d42f 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -250,8 +250,10 @@ async function getVerifiedFetch (): Promise { const verifiedFetch = await createVerifiedFetch({ gateways: config.gateways, - routers: config.routers, - dnsResolvers + routers: config.routers ?? [], + dnsResolvers, + allowInsecure: true, + allowLocal: true }, { contentTypeParser }) From e07d0cb6209c2112ef2ea7520d6b0549bb43990c Mon Sep 17 00:00:00 2001 From: Daniel Norman <1992255+2color@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:19:47 +0200 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: Marcin Rataj --- src/lib/local-gateway.ts | 2 +- src/pages/config.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/local-gateway.ts b/src/lib/local-gateway.ts index a8eb4601..11b0d14f 100644 --- a/src/lib/local-gateway.ts +++ b/src/lib/local-gateway.ts @@ -1,6 +1,6 @@ import { uiLogger } from './logger.js' -export const localGwUrl = 'http://127.0.0.1:8080' +export const localGwUrl = 'http://localhost:8080' // export const localGwUrl = 'http://localhost:8080' const localGwTestUrl = `${localGwUrl}/ipfs/bafkqablimvwgy3y?format=raw` const expectedContentType = 'application/vnd.ipld.raw' diff --git a/src/pages/config.tsx b/src/pages/config.tsx index 400f4e70..8a37ad86 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -115,7 +115,7 @@ function ConfigPage (): React.JSX.Element | null { const gwConf = unparsedGwConf != null ? JSON.parse(unparsedGwConf) as string[] : defaultGateways if (!gwConf.includes(localGwUrl)) { log(`Adding ${localGwUrl} to gateway list`) - gwConf.push(localGwUrl) + gwConf.unshift(localGwUrl) } // Update localStorage localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(gwConf)) From c0270313f4ee12e6ab4e59c7ba515a1e61f56abd Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:44:54 +0200 Subject: [PATCH 06/10] feat: add toggle switch for delegated routing --- src/lib/config-db.ts | 33 ++++++++++++++++++++++++--------- src/lib/local-storage.ts | 1 + src/pages/config.tsx | 1 + src/sw.ts | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/lib/config-db.ts b/src/lib/config-db.ts index aa9f4a93..a7b585e1 100644 --- a/src/lib/config-db.ts +++ b/src/lib/config-db.ts @@ -8,14 +8,17 @@ export interface ConfigDb extends BaseDbConfig { routers: string[] dnsJsonResolvers: Record autoReload: boolean + delegatedRouting: boolean debug: string } export const defaultGateways = ['https://trustless-gateway.link'] -export const defaultRouters = [] +export const defaultRouters = ['https://delegated-ipfs.dev'] export const defaultDnsJsonResolvers = { '.': 'https://delegated-ipfs.dev/dns-query' } +export const defaultdelegatedRouting = true +export const defaultAutoReload = false const configDb = new GenericIDB('helia-sw', 'config') @@ -26,7 +29,10 @@ export async function loadConfigFromLocalStorage (): Promise { const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? JSON.stringify(defaultGateways) const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? JSON.stringify(defaultRouters) const localStorageDnsResolvers = localStorage.getItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers) ?? JSON.stringify(defaultDnsJsonResolvers) - const autoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) === 'true' + const lsDelegatedRouting = localStorage.getItem(LOCAL_STORAGE_KEYS.config.delegatedRouting) + const delegatedRouting = lsDelegatedRouting === null ? defaultAutoReload : lsDelegatedRouting === 'true' + const lsAutoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) + const autoReload = lsAutoReload === null ? defaultAutoReload : lsAutoReload === 'true' const debug = localStorage.getItem(LOCAL_STORAGE_KEYS.config.debug) ?? '' const gateways = JSON.parse(localStorageGatewaysString) const routers = JSON.parse(localStorageRoutersString) @@ -35,6 +41,7 @@ export async function loadConfigFromLocalStorage (): Promise { await configDb.put('gateways', gateways) await configDb.put('routers', routers) + await configDb.put('delegatedRouting', delegatedRouting) await configDb.put('dnsJsonResolvers', dnsJsonResolvers) await configDb.put('autoReload', autoReload) await configDb.put('debug', debug) @@ -44,15 +51,17 @@ export async function loadConfigFromLocalStorage (): Promise { export async function resetConfig (): Promise { await configDb.open() - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.gateways) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(defaultGateways)) await configDb.put('gateways', defaultGateways) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.routers) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.routers, JSON.stringify(defaultRouters)) await configDb.put('routers', defaultRouters) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.delegatedRouting, String(defaultdelegatedRouting)) + await configDb.put('delegatedRouting', true) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers, JSON.stringify(defaultDnsJsonResolvers)) await configDb.put('dnsJsonResolvers', defaultDnsJsonResolvers) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.autoReload) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.autoReload, String(defaultAutoReload)) await configDb.put('autoReload', false) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.debug) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.debug, '') await configDb.put('debug', '') configDb.close() } @@ -65,6 +74,7 @@ export async function setConfig (config: ConfigDb, logger: ComponentLogger): Pro await configDb.open() await configDb.put('gateways', config.gateways) await configDb.put('routers', config.routers) + await configDb.put('delegatedRouting', config.delegatedRouting) await configDb.put('dnsJsonResolvers', config.dnsJsonResolvers) await configDb.put('autoReload', config.autoReload) await configDb.put('debug', config.debug ?? '') @@ -76,7 +86,8 @@ export async function getConfig (logger: ComponentLogger): Promise { let gateways: string[] = defaultGateways let routers: string[] = defaultRouters let dnsJsonResolvers: Record = defaultDnsJsonResolvers - let autoReload = false + let autoReload = defaultAutoReload + let delegatedRouting = defaultdelegatedRouting let debug = '' try { @@ -88,7 +99,10 @@ export async function getConfig (logger: ComponentLogger): Promise { dnsJsonResolvers = await configDb.get('dnsJsonResolvers') - autoReload = await configDb.get('autoReload') ?? false + autoReload = await configDb.get('autoReload') ?? defaultAutoReload + + delegatedRouting = await configDb.get('delegatedRouting') ?? defaultdelegatedRouting + debug = await configDb.get('debug') ?? '' configDb.close() debugLib.enable(debug) @@ -111,6 +125,7 @@ export async function getConfig (logger: ComponentLogger): Promise { return { gateways, routers, + delegatedRouting, dnsJsonResolvers, autoReload, debug diff --git a/src/lib/local-storage.ts b/src/lib/local-storage.ts index 68c590a1..8f6c6588 100644 --- a/src/lib/local-storage.ts +++ b/src/lib/local-storage.ts @@ -9,6 +9,7 @@ export const LOCAL_STORAGE_KEYS = { gateways: getLocalStorageKey('config', 'gateways'), routers: getLocalStorageKey('config', 'routers'), autoReload: getLocalStorageKey('config', 'autoReload'), + delegatedRouting: getLocalStorageKey('config', 'delegatedRouting'), dnsJsonResolvers: getLocalStorageKey('config', 'dnsJsonResolvers'), debug: getLocalStorageKey('config', 'debug') }, diff --git a/src/pages/config.tsx b/src/pages/config.tsx index 8a37ad86..4219a4c1 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -163,6 +163,7 @@ function ConfigPage (): React.JSX.Element | null { + diff --git a/src/sw.ts b/src/sw.ts index f461d42f..28ad655b 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -250,7 +250,7 @@ async function getVerifiedFetch (): Promise { const verifiedFetch = await createVerifiedFetch({ gateways: config.gateways, - routers: config.routers ?? [], + routers: config.delegatedRouting ? config.routers : [], dnsResolvers, allowInsecure: true, allowLocal: true From ddeb65fd203110d4eee0bc3ab51698adaba18d00 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:25:05 +0200 Subject: [PATCH 07/10] fix: tests --- test-e2e/layout.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-e2e/layout.test.ts b/test-e2e/layout.test.ts index c7efd7a6..dc1ff8fb 100644 --- a/test-e2e/layout.test.ts +++ b/test-e2e/layout.test.ts @@ -31,7 +31,7 @@ test.describe('smoketests', () => { const inputLocator = getConfigPageInput(page) // see https://playwright.dev/docs/locators#strictness await inputLocator.first().waitFor() - expect(await inputLocator.count()).toEqual(5) + expect(await inputLocator.count()).toEqual(6) const submitButton = getConfigPageSaveButton(page) await expect(submitButton).toBeVisible() }) From e9d04de497502f58ca804de4e301c91ce1b606bb Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:27:09 +0200 Subject: [PATCH 08/10] fix: add patch fix to allow local and insecure gateways --- patches/@helia+block-brokers+3.0.0.patch | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 patches/@helia+block-brokers+3.0.0.patch diff --git a/patches/@helia+block-brokers+3.0.0.patch b/patches/@helia+block-brokers+3.0.0.patch new file mode 100644 index 00000000..0ad0de04 --- /dev/null +++ b/patches/@helia+block-brokers+3.0.0.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js b/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js +index 223f634..34ff22d 100644 +--- a/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js ++++ b/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js +@@ -58,10 +58,14 @@ export class TrustlessGatewayBlockBroker { + } + } + createSession(options = {}) { +- return createTrustlessGatewaySession({ +- logger: this.logger, +- routing: this.routing +- }, options); ++ return createTrustlessGatewaySession({ ++ logger: this.logger, ++ routing: this.routing ++ }, { ++ ...options, ++ allowLocal: this.allowLocal, ++ allowInsecure: this.allowInsecure ++ }) + } + } + //# sourceMappingURL=broker.js.map +\ No newline at end of file From 4a160116632323e7547f6294ebe50d1711a84a8e Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:39:33 +0200 Subject: [PATCH 09/10] fix: disable caching when probing local gateway --- src/lib/local-gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/local-gateway.ts b/src/lib/local-gateway.ts index 11b0d14f..1695001a 100644 --- a/src/lib/local-gateway.ts +++ b/src/lib/local-gateway.ts @@ -11,7 +11,7 @@ const log = uiLogger.forComponent('local-gateway-prober') export async function hasLocalGateway (): Promise { try { log(`probing for local trustless gateway at ${localGwTestUrl}`) - const resp = await fetch(localGwTestUrl) + const resp = await fetch(localGwTestUrl, { cache: 'no-store' }) if (!resp.ok) { return false } From 47f2a3c57adf749988e2247be02cd9aa136fad97 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:40:54 +0200 Subject: [PATCH 10/10] feat: remove local gateway if unavailable --- src/pages/config.tsx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/config.tsx b/src/pages/config.tsx index 4219a4c1..d18acd54 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -106,24 +106,35 @@ function ConfigPage (): React.JSX.Element | null { log.trace('RELOAD_CONFIG sent to parent window') }, []) + // Effect to add or remove the local gateway useEffect(() => { hasLocalGateway() .then(async hasLocalGw => { + // check if local storage has it. + const unparsedGwConf = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) + let gwConf = unparsedGwConf != null ? JSON.parse(unparsedGwConf) as string[] : defaultGateways + if (hasLocalGw) { - // check if local storage has it. - const unparsedGwConf = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) - const gwConf = unparsedGwConf != null ? JSON.parse(unparsedGwConf) as string[] : defaultGateways + // Add the local gateway to config if not there already if (!gwConf.includes(localGwUrl)) { log(`Adding ${localGwUrl} to gateway list`) gwConf.unshift(localGwUrl) } - // Update localStorage - localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(gwConf)) - await loadConfigFromLocalStorage() - await channel.messageAndWaitForResponse('SW', { target: 'SW', action: 'RELOAD_CONFIG' }) - await postFromIframeToParentSw() - setResetKey((prev) => prev + 1) + } else if (gwConf.includes(localGwUrl)) { + // remove local gateway from the configuration if the gateway is not available + gwConf = gwConf.filter(gw => gw !== localGwUrl) + if (gwConf.length === 0) { + // if there are no gateways following the removal reset to the default gateways + gwConf = defaultGateways + } } + + // persist to localstorage, idb and 🙃 + localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(gwConf)) + await loadConfigFromLocalStorage() + await channel.messageAndWaitForResponse('SW', { target: 'SW', action: 'RELOAD_CONFIG' }) + await postFromIframeToParentSw() + setResetKey((prev) => prev + 1) // needed to ensure the config is re-rendered }).catch(err => { log.error('failed to probe for local gateway', err) })