feat(settings): add settings for custom DNS servers and IPv4 resolution first (#1266)

* feat(settings): add settings for custom DNS servers and IPv4 resolution first

This PR adds settings to change the DNS servers Jellyseerr uses and to force Jellyseerr to resolve
DNS queries using IPv4 first. These settings aim to make it easier for less experienced users to fix
network errors related to DNS resolution.

* style: fix missing newline
This commit is contained in:
Gauthier
2025-01-16 10:46:27 +01:00
committed by GitHub
parent 5d9f613dd8
commit 7fcc0eb66d
7 changed files with 91 additions and 6 deletions

View File

@@ -23,6 +23,8 @@
"mediaServerType": 1, "mediaServerType": 1,
"partialRequestsEnabled": true, "partialRequestsEnabled": true,
"enableSpecialEpisodes": false, "enableSpecialEpisodes": false,
"forceIpv4First": false,
"dnsServers": "",
"locale": "en" "locale": "en"
}, },
"plex": { "plex": {

View File

@@ -191,6 +191,12 @@ components:
enableSpecialEpisodes: enableSpecialEpisodes:
type: boolean type: boolean
example: false example: false
forceIpv4First:
type: boolean
example: false
dnsServers:
type: string
example: '1.1.1.1'
PlexLibrary: PlexLibrary:
type: object type: object
properties: properties:

View File

@@ -41,11 +41,6 @@ import path from 'path';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs'; import YAML from 'yamljs';
if (process.env.forceIpv4First === 'true') {
dns.setDefaultResultOrder('ipv4first');
net.setDefaultAutoSelectFamily(false);
}
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`); logger.info(`Starting Overseerr version ${getAppVersion()}`);
@@ -79,6 +74,18 @@ app
const settings = await getSettings().load(); const settings = await getSettings().load();
restartFlag.initializeSettings(settings.main); restartFlag.initializeSettings(settings.main);
// Check if we force IPv4 first
if (process.env.forceIpv4First === 'true' || settings.main.forceIpv4First) {
dns.setDefaultResultOrder('ipv4first');
net.setDefaultAutoSelectFamily(false);
}
if (settings.main.dnsServers.trim() !== '') {
dns.setServers(
settings.main.dnsServers.split(',').map((server) => server.trim())
);
}
// Register HTTP proxy // Register HTTP proxy
if (settings.main.proxy.enabled) { if (settings.main.proxy.enabled) {
await createCustomProxyAgent(settings.main.proxy); await createCustomProxyAgent(settings.main.proxy);

View File

@@ -132,6 +132,8 @@ export interface MainSettings {
mediaServerType: number; mediaServerType: number;
partialRequestsEnabled: boolean; partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean; enableSpecialEpisodes: boolean;
forceIpv4First: boolean;
dnsServers: string;
locale: string; locale: string;
proxy: ProxySettings; proxy: ProxySettings;
} }
@@ -346,6 +348,8 @@ class Settings {
mediaServerType: MediaServerType.NOT_CONFIGURED, mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true, partialRequestsEnabled: true,
enableSpecialEpisodes: false, enableSpecialEpisodes: false,
forceIpv4First: false,
dnsServers: '',
locale: 'en', locale: 'en',
proxy: { proxy: {
enabled: false, enabled: false,

View File

@@ -14,7 +14,9 @@ class RestartFlag {
return ( return (
this.settings.csrfProtection !== settings.csrfProtection || this.settings.csrfProtection !== settings.csrfProtection ||
this.settings.trustProxy !== settings.trustProxy || this.settings.trustProxy !== settings.trustProxy ||
this.settings.proxy.enabled !== settings.proxy.enabled this.settings.proxy.enabled !== settings.proxy.enabled ||
this.settings.forceIpv4First !== settings.forceIpv4First ||
this.settings.dnsServers !== settings.dnsServers
); );
} }
} }

View File

@@ -57,6 +57,12 @@ const messages = defineMessages('components.Settings.SettingsMain', {
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
partialRequestsEnabled: 'Allow Partial Series Requests', partialRequestsEnabled: 'Allow Partial Series Requests',
enableSpecialEpisodes: 'Allow Special Episodes Requests', enableSpecialEpisodes: 'Allow Special Episodes Requests',
forceIpv4First: 'IPv4 Resolution First',
forceIpv4FirstTip:
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
dnsServers: 'Custom DNS Servers',
dnsServersTip:
'Comma-separated list of custom DNS servers, e.g. "1.1.1.1,[2606:4700:4700::1111]"',
locale: 'Display Language', locale: 'Display Language',
proxyEnabled: 'HTTP(S) Proxy', proxyEnabled: 'HTTP(S) Proxy',
proxyHostname: 'Proxy Hostname', proxyHostname: 'Proxy Hostname',
@@ -160,6 +166,8 @@ const SettingsMain = () => {
streamingRegion: data?.streamingRegion || 'US', streamingRegion: data?.streamingRegion || 'US',
partialRequestsEnabled: data?.partialRequestsEnabled, partialRequestsEnabled: data?.partialRequestsEnabled,
enableSpecialEpisodes: data?.enableSpecialEpisodes, enableSpecialEpisodes: data?.enableSpecialEpisodes,
forceIpv4First: data?.forceIpv4First,
dnsServers: data?.dnsServers,
trustProxy: data?.trustProxy, trustProxy: data?.trustProxy,
cacheImages: data?.cacheImages, cacheImages: data?.cacheImages,
proxyEnabled: data?.proxy?.enabled, proxyEnabled: data?.proxy?.enabled,
@@ -191,6 +199,8 @@ const SettingsMain = () => {
originalLanguage: values.originalLanguage, originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled, partialRequestsEnabled: values.partialRequestsEnabled,
enableSpecialEpisodes: values.enableSpecialEpisodes, enableSpecialEpisodes: values.enableSpecialEpisodes,
forceIpv4First: values.forceIpv4First,
dnsServers: values.dnsServers,
trustProxy: values.trustProxy, trustProxy: values.trustProxy,
cacheImages: values.cacheImages, cacheImages: values.cacheImages,
proxy: { proxy: {
@@ -524,6 +534,55 @@ const SettingsMain = () => {
/> />
</div> </div>
</div> </div>
<div className="form-row">
<label htmlFor="forceIpv4First" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.forceIpv4First)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.forceIpv4FirstTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="forceIpv4First"
name="forceIpv4First"
onChange={() => {
setFieldValue('forceIpv4First', !values.forceIpv4First);
}}
/>
</div>
</div>
<div className="form-row">
<label htmlFor="dnsServers" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.dnsServers)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.dnsServersTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="dnsServers"
name="dnsServers"
type="text"
inputMode="url"
/>
</div>
{errors.dnsServers &&
touched.dnsServers &&
typeof errors.dnsServers === 'string' && (
<div className="error">{errors.dnsServers}</div>
)}
</div>
</div>
<div className="form-row"> <div className="form-row">
<label htmlFor="proxyEnabled" className="checkbox-label"> <label htmlFor="proxyEnabled" className="checkbox-label">
<span className="mr-2"> <span className="mr-2">

View File

@@ -246,6 +246,7 @@
"components.Login.initialsigningin": "Connecting…", "components.Login.initialsigningin": "Connecting…",
"components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.", "components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.",
"components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.loginerror": "Something went wrong while trying to sign in.",
"components.Login.noadminerror": "No admin user found on the server.",
"components.Login.password": "Password", "components.Login.password": "Password",
"components.Login.port": "Port", "components.Login.port": "Port",
"components.Login.save": "Add", "components.Login.save": "Add",
@@ -919,7 +920,11 @@
"components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", "components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.SettingsMain.discoverRegion": "Discover Region", "components.Settings.SettingsMain.discoverRegion": "Discover Region",
"components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability", "components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability",
"components.Settings.SettingsMain.dnsServers": "Custom DNS Servers",
"components.Settings.SettingsMain.dnsServersTip": "Comma-separated list of custom DNS servers, e.g. \"1.1.1.1,[2606:4700:4700::1111]\"",
"components.Settings.SettingsMain.enableSpecialEpisodes": "Allow Special Episodes Requests", "components.Settings.SettingsMain.enableSpecialEpisodes": "Allow Special Episodes Requests",
"components.Settings.SettingsMain.forceIpv4First": "IPv4 Resolution First",
"components.Settings.SettingsMain.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6",
"components.Settings.SettingsMain.general": "General", "components.Settings.SettingsMain.general": "General",
"components.Settings.SettingsMain.generalsettings": "General Settings", "components.Settings.SettingsMain.generalsettings": "General Settings",
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.", "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",