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
77# Listen for incoming connections
88listen: false
99# 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
1012protocol:
1113 ipv4: trueauto
1214 ipv6: falseauto
1315# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
1416dnsPreferIPv6: false
1517# The hostname that autorun opens.
server.js+147 -30
@@ -4,6 +4,7 @@
44import fs from 'node:fs';
55import http from 'node:http';
66import https from 'node:https';
7+import os from 'os';
78import path from 'node:path';
89import util from 'node:util';
910import net from 'node:net';
@@ -67,6 +68,8 @@ import {
6768 forwardFetchResponse,
6869 removeColorFormatting,
6970 getSeparator,
71+ stringToBool,
72+ urlHostnameToIPv6,
7073} from './src/util.js';
7174import { UPLOADS_DIRECTORY } from './src/constants.js';
7275import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
@@ -133,8 +136,8 @@ const DEFAULT_CSRF_DISABLED = false;
133136const DEFAULT_BASIC_AUTH = false;
134137const DEFAULT_PER_USER_BASIC_AUTH = false;
135138
136139const DEFAULT_ENABLE_IPV6 = false'auto';
137140const DEFAULT_ENABLE_IPV4 = true'auto';
138141
139142const DEFAULT_PREFER_IPV6 = false;
140143
@@ -150,11 +153,11 @@ const DEFAULT_PROXY_BYPASS = [];
150153const cliArguments = yargs(hideBin(process.argv))
151154 .usage('Usage: <your-start-script> <command> [options]')
152155 .option('enableIPv6', {
153156 type: 'booleanstring',
154157 default: null,
155158 describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
156159 }).option('enableIPv4', {
157160 type: 'booleanstring',
158161 default: null,
159162 describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
160163 }).option('port', {
@@ -243,6 +246,7 @@ app.use(helmet({
243246app.use(compression());
244247app.use(responseTime());
245248
249+
246250const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
247251const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
248252const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
@@ -256,8 +260,9 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
256260
257261const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
258262
259-const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
263+
260264constlet enableIPv4enableIPv6 = stringToBool(cliArguments.enableIPv4enableIPv6) ?? getConfigValue('protocol.ipv4ipv6', DEFAULT_ENABLE_IPV4DEFAULT_ENABLE_IPV6);
265+let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
261266
262267const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
263268const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
@@ -280,7 +285,19 @@ if (dnsPreferIPv6) {
280285 console.log('Preferring IPv4 for DNS resolution');
281286}
282287
283-if (!enableIPv6 && !enableIPv4) {
288+
289+const ipOptions = [true, 'auto', false];
290+
291+if (!ipOptions.includes(enableIPv6)) {
292+ console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', ipOptions, '\n setting to: auto');
293+ enableIPv6 = 'auto';
294+}
295+if (!ipOptions.includes(enableIPv4)) {
296+ console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', ipOptions, '\n setting to: auto');
297+ enableIPv4 = 'auto';
298+}
299+
300+if (enableIPv6 === false && enableIPv4 === false) {
284301 console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
285302 process.exit(1);
286303}
@@ -365,6 +382,40 @@ function getSessionCookieAge() {
365382 return undefined;
366383}
367384
385+async 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+
368419app.use(cookieSession({
369420 name: getCookieSessionName(),
370421 sameSite: 'strict',
@@ -685,18 +736,18 @@ const preSetupTasks = async function () {
685736 * Gets the hostname to use for autorun in the browser.
686737 * @returns {string} The hostname to use for autorun
687738 */
688739function getAutorunHostname(useIPv6, useIPv4) {
689740 if (autorunHostname === 'auto') {
690741 if (enableIPv6useIPv6 && enableIPv4useIPv4) {
691742 if (avoidLocalhost) return '[::1]';
692743 return 'localhost';
693744 }
694745
695746 if (enableIPv6useIPv6) {
696747 return '[::1]';
697748 }
698749
699750 if (enableIPv4useIPv4) {
700751 return '127.0.0.1';
701752 }
702753 }
@@ -709,10 +760,10 @@ function getAutorunHostname() {
709760 * @param {boolean} v6Failed If the server failed to start on IPv6
710761 * @param {boolean} v4Failed If the server failed to start on IPv4
711762 */
712763const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
713764 const autorunUrl = new URL(
714765 (cliArguments.ssl ? 'https://' : 'http://') +
715766 (getAutorunHostname(useIPv6, useIPv4)) +
716767 (':') +
717768 ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
718769 );
@@ -725,11 +776,11 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
725776
726777 let logListen = 'SillyTavern is listening on';
727778
728779 if (enableIPv6useIPv6 && !v6Failed) {
729780 logListen += color.green(' IPv6: ' + tavernUrlV6.host);
730781 }
731782
732783 if (enableIPv4useIPv4 && !v4Failed) {
733784 logListen += color.green(' IPv4: ' + tavernUrl.host);
734785 }
735786
@@ -805,13 +856,13 @@ function logSecurityAlert(message) {
805856 * @param {boolean} v6Failed If the server failed to start on IPv6
806857 * @param {boolean} v4Failed If the server failed to start on IPv4
807858 */
808859function handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4) {
809860 if (v6Failed && !enableIPv4useIPv4) {
810861 console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
811862 process.exit(1);
812863 }
813864
814865 if (v4Failed && !enableIPv6useIPv6) {
815866 console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
816867 process.exit(1);
817868 }
@@ -828,7 +879,7 @@ function handleServerListenFail(v6Failed, v4Failed) {
828879 * @returns {Promise<void>} A promise that resolves when the server is listening
829880 * @throws {Error} If the server fails to start
830881 */
831882function createHttpsServer(url, ipVersion) {
832883 return new Promise((resolve, reject) => {
833884 const server = https.createServer(
834885 {
@@ -837,7 +888,15 @@ function createHttpsServer(url) {
837888 }, app);
838889 server.on('error', reject);
839890 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+ });
841900 });
842901}
843902
@@ -847,24 +906,32 @@ function createHttpsServer(url) {
847906 * @returns {Promise<void>} A promise that resolves when the server is listening
848907 * @throws {Error} If the server fails to start
849908 */
850909function createHttpServer(url, ipVersion) {
851910 return new Promise((resolve, reject) => {
852911 const server = http.createServer(app);
853912 server.on('error', reject);
854913 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+ });
856923 });
857924}
858925
859926async function startHTTPorHTTPS(useIPv6, useIPv4) {
860927 let v6Failed = false;
861928 let v4Failed = false;
862929
863930 const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
864931
865932 if (enableIPv6useIPv6) {
866933 try {
867934 await createFunc(tavernUrlV6, 6);
868935 } catch (error) {
869936 console.error('non-fatal error: failed to start server on IPv6');
870937 console.error(error);
@@ -873,9 +940,9 @@ async function startHTTPorHTTPS() {
873940 }
874941 }
875942
876943 if (enableIPv4useIPv4) {
877944 try {
878945 await createFunc(tavernUrl, 4);
879946 } catch (error) {
880947 console.error('non-fatal error: failed to start server on IPv4');
881948 console.error(error);
@@ -888,10 +955,60 @@ async function startHTTPorHTTPS() {
888955}
889956
890957async 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
8931010 handleServerListenFail(v6Failed, v4Failed, useIPv6, useIPv4);
8941011 postSetupTasks(v6Failed, v4Failed, useIPv6, useIPv4);
8951012}
8961013
8971014async function verifySecuritySettings() {
src/util.js+16 -0
@@ -692,6 +692,22 @@ export function isValidUrl(url) {
692692 }
693693}
694694
695+export 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+
705+export function stringToBool(str) {
706+ if (str === 'true') return true;
707+ if (str === 'false') return false;
708+ return str;
709+}
710+
695711/**
696712 * MemoryLimitedMap class that limits the memory usage of string values.
697713 */