Merge pull request #3226 from BPplays/ipv6_auto Automatically detect what IP versions are available and use them

0e3ff1699a4dfec3ed8fad962bad898836f9b315

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +167 -32Ignore whitespace
default/config.yaml+4 -2
@@ -7,9 +7,11 @@ cardsCacheCapacity: 100
7# Listen for incoming connections7# Listen for incoming connections
8listen: false8listen: false
9# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!9# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
10# - Use option "auto" to automatically detect support
11# - Use true or false (no qoutes) to enable or disable each protocol
10protocol:12protocol:
11 ipv4: true13 ipv4: auto
12 ipv6: false14 ipv6: auto
13# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv615# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
14dnsPreferIPv6: false16dnsPreferIPv6: false
15# The hostname that autorun opens.17# The hostname that autorun opens.
server.js+147 -30
@@ -4,6 +4,7 @@
4import fs from 'node:fs';4import fs from 'node:fs';
5import http from 'node:http';5import http from 'node:http';
6import https from 'node:https';6import https from 'node:https';
7import os from 'os';
7import path from 'node:path';8import path from 'node:path';
8import util from 'node:util';9import util from 'node:util';
9import net from 'node:net';10import net from 'node:net';
@@ -67,6 +68,8 @@ import {
67 forwardFetchResponse,68 forwardFetchResponse,
68 removeColorFormatting,69 removeColorFormatting,
69 getSeparator,70 getSeparator,
71 stringToBool,
72 urlHostnameToIPv6,
70} from './src/util.js';73} from './src/util.js';
71import { UPLOADS_DIRECTORY } from './src/constants.js';74import { UPLOADS_DIRECTORY } from './src/constants.js';
72import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';75import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -133,8 +136,8 @@ const DEFAULT_CSRF_DISABLED = false;
133const DEFAULT_BASIC_AUTH = false;136const DEFAULT_BASIC_AUTH = false;
134const DEFAULT_PER_USER_BASIC_AUTH = false;137const DEFAULT_PER_USER_BASIC_AUTH = false;
135138
136const DEFAULT_ENABLE_IPV6 = false;139const DEFAULT_ENABLE_IPV6 = 'auto';
137const DEFAULT_ENABLE_IPV4 = true;140const DEFAULT_ENABLE_IPV4 = 'auto';
138141
139const DEFAULT_PREFER_IPV6 = false;142const DEFAULT_PREFER_IPV6 = false;
140143
@@ -150,11 +153,11 @@ const DEFAULT_PROXY_BYPASS = [];
150const cliArguments = yargs(hideBin(process.argv))153const cliArguments = yargs(hideBin(process.argv))
151 .usage('Usage: <your-start-script> <command> [options]')154 .usage('Usage: <your-start-script> <command> [options]')
152 .option('enableIPv6', {155 .option('enableIPv6', {
153 type: 'boolean',156 type: 'string',
154 default: null,157 default: null,
155 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,158 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
156 }).option('enableIPv4', {159 }).option('enableIPv4', {
157 type: 'boolean',160 type: 'string',
158 default: null,161 default: null,
159 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,162 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
160 }).option('port', {163 }).option('port', {
@@ -243,6 +246,7 @@ app.use(helmet({
243app.use(compression());246app.use(compression());
244app.use(responseTime());247app.use(responseTime());
245248
249
246const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);250const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
247const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;251const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
248const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);252const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
@@ -256,8 +260,9 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
256260
257const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);261const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
258262
259const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);263
260const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);264let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
265let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
261266
262const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);267const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
263const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);268const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
@@ -280,7 +285,19 @@ if (dnsPreferIPv6) {
280 console.log('Preferring IPv4 for DNS resolution');285 console.log('Preferring IPv4 for DNS resolution');
281}286}
282287
283if (!enableIPv6 && !enableIPv4) {288
289const ipOptions = [true, 'auto', false];
290
291if (!ipOptions.includes(enableIPv6)) {
292 console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to: auto');
293 enableIPv6 = 'auto';
294}
295if (!ipOptions.includes(enableIPv4)) {
296 console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to: auto');
297 enableIPv4 = 'auto';
298}
299
300if (enableIPv6 === false && enableIPv4 === false) {
284 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');301 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
285 process.exit(1);302 process.exit(1);
286}303}
@@ -365,6 +382,40 @@ function getSessionCookieAge() {
365 return undefined;382 return undefined;
366}383}
367384
385async function getHasIP() {
386 let hasIPv6 = false;
387 let hasIPv6Local = false;
388
389 let hasIPv4 = false;
390 let hasIPv4Local = false;
391
392 const interfaces = os.networkInterfaces();
393
394 for (const iface of Object.values(interfaces)) {
395 if (iface === undefined) {
396 continue;
397 }
398 for (const info of iface) {
399 if (info.family === 'IPv6') {
400 hasIPv6 = true;
401 if (info.address === '::1') {
402 hasIPv6Local = true;
403 }
404 }
405
406 if (info.family === 'IPv4') {
407 hasIPv4 = true;
408 if (info.address === '127.0.0.1') {
409 hasIPv4Local = true;
410 }
411 }
412 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
413 }
414 if (hasIPv6 && hasIPv4 && hasIPv6Local && hasIPv4Local) break;
415 }
416 return [hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local];
417}
418
368app.use(cookieSession({419app.use(cookieSession({
369 name: getCookieSessionName(),420 name: getCookieSessionName(),
370 sameSite: 'strict',421 sameSite: 'strict',
@@ -685,18 +736,18 @@ const preSetupTasks = async function () {
685 * Gets the hostname to use for autorun in the browser.736 * Gets the hostname to use for autorun in the browser.
686 * @returns {string} The hostname to use for autorun737 * @returns {string} The hostname to use for autorun
687 */738 */
688function getAutorunHostname() {739function getAutorunHostname(useIPv6, useIPv4) {
689 if (autorunHostname === 'auto') {740 if (autorunHostname === 'auto') {
690 if (enableIPv6 && enableIPv4) {741 if (useIPv6 && useIPv4) {
691 if (avoidLocalhost) return '[::1]';742 if (avoidLocalhost) return '[::1]';
692 return 'localhost';743 return 'localhost';
693 }744 }
694745
695 if (enableIPv6) {746 if (useIPv6) {
696 return '[::1]';747 return '[::1]';
697 }748 }
698749
699 if (enableIPv4) {750 if (useIPv4) {
700 return '127.0.0.1';751 return '127.0.0.1';
701 }752 }
702 }753 }
@@ -709,10 +760,10 @@ function getAutorunHostname() {
709 * @param {boolean} v6Failed If the server failed to start on IPv6760 * @param {boolean} v6Failed If the server failed to start on IPv6
710 * @param {boolean} v4Failed If the server failed to start on IPv4761 * @param {boolean} v4Failed If the server failed to start on IPv4
711 */762 */
712const postSetupTasks = async function (v6Failed, v4Failed) {763const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
713 const autorunUrl = new URL(764 const autorunUrl = new URL(
714 (cliArguments.ssl ? 'https://' : 'http://') +765 (cliArguments.ssl ? 'https://' : 'http://') +
715 (getAutorunHostname()) +766 (getAutorunHostname(useIPv6, useIPv4)) +
716 (':') +767 (':') +
717 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),768 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
718 );769 );
@@ -725,11 +776,11 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
725776
726 let logListen = 'SillyTavern is listening on';777 let logListen = 'SillyTavern is listening on';
727778
728 if (enableIPv6 && !v6Failed) {779 if (useIPv6 && !v6Failed) {
729 logListen += color.green(' IPv6: ' + tavernUrlV6.host);780 logListen += color.green(' IPv6: ' + tavernUrlV6.host);
730 }781 }
731782
732 if (enableIPv4 && !v4Failed) {783 if (useIPv4 && !v4Failed) {
733 logListen += color.green(' IPv4: ' + tavernUrl.host);784 logListen += color.green(' IPv4: ' + tavernUrl.host);
734 }785 }
735786
@@ -805,13 +856,13 @@ function logSecurityAlert(message) {
805 * @param {boolean} v6Failed If the server failed to start on IPv6856 * @param {boolean} v6Failed If the server failed to start on IPv6
806 * @param {boolean} v4Failed If the server failed to start on IPv4857 * @param {boolean} v4Failed If the server failed to start on IPv4
807 */858 */
808function handleServerListenFail(v6Failed, v4Failed) {859function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
809 if (v6Failed && !enableIPv4) {860 if (v6Failed && !useIPv4) {
810 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));861 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
811 process.exit(1);862 process.exit(1);
812 }863 }
813864
814 if (v4Failed && !enableIPv6) {865 if (v4Failed && !useIPv6) {
815 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));866 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
816 process.exit(1);867 process.exit(1);
817 }868 }
@@ -828,7 +879,7 @@ function handleServerListenFail(v6Failed, v4Failed) {
828 * @returns {Promise<void>} A promise that resolves when the server is listening879 * @returns {Promise<void>} A promise that resolves when the server is listening
829 * @throws {Error} If the server fails to start880 * @throws {Error} If the server fails to start
830 */881 */
831function createHttpsServer(url) {882function createHttpsServer(url, ipVersion) {
832 return new Promise((resolve, reject) => {883 return new Promise((resolve, reject) => {
833 const server = https.createServer(884 const server = https.createServer(
834 {885 {
@@ -837,7 +888,15 @@ function createHttpsServer(url) {
837 }, app);888 }, app);
838 server.on('error', reject);889 server.on('error', reject);
839 server.on('listening', resolve);890 server.on('listening', resolve);
840 server.listen(Number(url.port || 443), url.hostname);891
892 let host = url.hostname;
893 if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
894 server.listen({
895 host: host,
896 port: Number(url.port || 443),
897 // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
898 ipv6Only: true,
899 });
841 });900 });
842}901}
843902
@@ -847,24 +906,32 @@ function createHttpsServer(url) {
847 * @returns {Promise<void>} A promise that resolves when the server is listening906 * @returns {Promise<void>} A promise that resolves when the server is listening
848 * @throws {Error} If the server fails to start907 * @throws {Error} If the server fails to start
849 */908 */
850function createHttpServer(url) {909function createHttpServer(url, ipVersion) {
851 return new Promise((resolve, reject) => {910 return new Promise((resolve, reject) => {
852 const server = http.createServer(app);911 const server = http.createServer(app);
853 server.on('error', reject);912 server.on('error', reject);
854 server.on('listening', resolve);913 server.on('listening', resolve);
855 server.listen(Number(url.port || 80), url.hostname);914
915 let host = url.hostname;
916 if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
917 server.listen({
918 host: host,
919 port: Number(url.port || 80),
920 // see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
921 ipv6Only: true,
922 });
856 });923 });
857}924}
858925
859async function startHTTPorHTTPS() {926async function startHTTPorHTTPS(useIPv6, useIPv4) {
860 let v6Failed = false;927 let v6Failed = false;
861 let v4Failed = false;928 let v4Failed = false;
862929
863 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;930 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
864931
865 if (enableIPv6) {932 if (useIPv6) {
866 try {933 try {
867 await createFunc(tavernUrlV6);934 await createFunc(tavernUrlV6, 6);
868 } catch (error) {935 } catch (error) {
869 console.error('non-fatal error: failed to start server on IPv6');936 console.error('non-fatal error: failed to start server on IPv6');
870 console.error(error);937 console.error(error);
@@ -873,9 +940,9 @@ async function startHTTPorHTTPS() {
873 }940 }
874 }941 }
875942
876 if (enableIPv4) {943 if (useIPv4) {
877 try {944 try {
878 await createFunc(tavernUrl);945 await createFunc(tavernUrl, 4);
879 } catch (error) {946 } catch (error) {
880 console.error('non-fatal error: failed to start server on IPv4');947 console.error('non-fatal error: failed to start server on IPv4');
881 console.error(error);948 console.error(error);
@@ -888,10 +955,60 @@ async function startHTTPorHTTPS() {
888}955}
889956
890async function startServer() {957async function startServer() {
891 const [v6Failed, v4Failed] = await startHTTPorHTTPS();958 let useIPv6 = (enableIPv6 === true);
959 let useIPv4 = (enableIPv4 === true);
960 let hasIPv6, hasIPv4, hasIPv6Local, hasIPv4Local, hasIPv6Any, hasIPv4Any;
961
962
963 if (enableIPv6 === 'auto' || enableIPv4 === 'auto') {
964 [hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local] = await getHasIP();
965
966 hasIPv6 = listen ? hasIPv6Any : hasIPv6Local;
967 if (enableIPv6 === 'auto') {
968 useIPv6 = hasIPv6;
969 }
970 if (hasIPv6) {
971 if (useIPv6) {
972 console.log(color.green('IPv6 support detected'));
973 } else {
974 console.log('IPv6 support detected (but disabled)');
975 }
976 }
977
978
979 hasIPv4 = listen ? hasIPv4Any : hasIPv4Local;
980 if (enableIPv4 === 'auto') {
981 useIPv4 = hasIPv4;
982 }
983 if (hasIPv4) {
984 if (useIPv4) {
985 console.log(color.green('IPv4 support detected'));
986 } else {
987 console.log('IPv4 support detected (but disabled)');
988 }
989 }
990 } else {
991 console.log("Neither protocol: ipv6, nor ipv4 are set to auto, skipping detection")
992 }
993
994
995
996 if (enableIPv6 === 'auto' && enableIPv4 === 'auto') {
997 if (!hasIPv6 && !hasIPv4) {
998 console.error('Both IPv6 and IPv4 are not detected');
999 process.exit(1);
1000 }
1001 }
1002
1003 if (!useIPv6 && !useIPv4) {
1004 console.error('Both IPv6 and IPv4 are disabled,\nP.S. you should never see this error, at least at one point it was checked for before this, with the rest of the config options');
1005 process.exit(1);
1006 }
1007
1008 const [v6Failed, v4Failed] = await startHTTPorHTTPS(useIPv6, useIPv4);
8921009
893 handleServerListenFail(v6Failed, v4Failed);1010 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
894 postSetupTasks(v6Failed, v4Failed);1011 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
895}1012}
8961013
897async function verifySecuritySettings() {1014async function verifySecuritySettings() {
src/util.js+16 -0
@@ -692,6 +692,22 @@ export function isValidUrl(url) {
692 }692 }
693}693}
694694
695export function urlHostnameToIPv6(hostname) {
696 if (hostname.startsWith('[')) {
697 hostname = hostname.slice(1);
698 }
699 if (hostname.endsWith(']')) {
700 hostname = hostname.slice(0, -1);
701 }
702 return hostname;
703}
704
705export function stringToBool(str) {
706 if (str === 'true') return true;
707 if (str === 'false') return false;
708 return str;
709}
710
695/**711/**
696 * MemoryLimitedMap class that limits the memory usage of string values.712 * MemoryLimitedMap class that limits the memory usage of string values.
697 */713 */