diff --git a/packages/certificates/package.json b/packages/certificates/package.json index ca90c237c..3a778ed51 100644 --- a/packages/certificates/package.json +++ b/packages/certificates/package.json @@ -23,6 +23,8 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", + "@homarr/log": "workspace:^0.1.0", + "cacheable-lookup": "^7.0.0", "undici": "7.3.0" }, "devDependencies": { diff --git a/packages/certificates/src/server.ts b/packages/certificates/src/server.ts index 9146896e1..3d9887284 100644 --- a/packages/certificates/src/server.ts +++ b/packages/certificates/src/server.ts @@ -8,6 +8,15 @@ import { fetch } from "undici"; import { env } from "@homarr/common/env"; import { LoggingAgent } from "@homarr/common/server"; +import CacheableLookup from "cacheable-lookup"; +import type { LookupOptions } from "node:dns"; +import { logger } from "@homarr/log"; + +const cacheableLookup = new CacheableLookup({ + maxTtl: 6_00, + errorTtl: 60, + fallbackDuration: 3600 +}); const getCertificateFolder = () => { return env.NODE_ENV === "production" @@ -66,15 +75,50 @@ export const createCertificateAgentAsync = async () => { return new LoggingAgent({ connect: { ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)), + // Override the DNS lookup with the https://www.npmjs.com/package/cacheable-lookup library lookup (see https://github.com/nodejs/undici/issues/2440#issuecomment-2181215481) + lookup(hostname, lookupOptions, callback) { + logger.info(`looking up ${hostname} with cacheable lookup`); + cacheableLookup.lookup(hostname, { all: lookupOptions.all, hints: lookupOptions.hints, family: convertToNodeJsIPFamily(lookupOptions.family) }, (err, address, family) => { + if (err) { + logger.error(`DNS Lookup failed for ${hostname}: ${err}`); + } else { + logger.info(`Cacheable Lookup for ${hostname}: ${JSON.stringify(address)}, family: ${family}`); + } + callback(err, address, family); + }); + } }, + keepAliveTimeout: 10_000, // Keep connection open for 10s + keepAliveMaxTimeout: 30_000, + pipelining: 0 }); }; +/** + * Converts the family + * @see https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback + * @param lookupOptionsFamily the family that should be converted + */ +const convertToNodeJsIPFamily = (lookupOptionsFamily: LookupOptions['family']) => { + switch (lookupOptionsFamily) { + case 'IPv4': { + return 4; + } + case 'IPv6': { + return 6; + } + default: { + return undefined; + } + } +} + export const createAxiosCertificateInstanceAsync = async () => { const customCertificates = await loadCustomRootCertificatesAsync(); return axios.create({ httpsAgent: new Agent({ ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)), + keepAlive: true }), }); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31677048d..897efb8f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -743,6 +743,12 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/log': + specifier: workspace:^0.1.0 + version: link:../log + cacheable-lookup: + specifier: ^7.0.0 + version: 7.0.0 undici: specifier: 7.3.0 version: 7.3.0 @@ -5286,6 +5292,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -13458,6 +13468,8 @@ snapshots: cac@6.7.14: {} + cacheable-lookup@7.0.0: {} + call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0