@@ -32,6 +32,9 @@ final class Config {
3232 private final int quicHandshakeTimeoutMs ;
3333 private final int quicTcpConnectTimeoutMs ;
3434 private final int quicIngressRingSlots ;
35+ private final String quicCongestionControl ;
36+ private final int quicInitialCwndPackets ;
37+ private final long quicStreamWindowBytes ;
3538 private final boolean camouflageTcpEnabled ;
3639 private final String camouflageTcpProxyHost ;
3740 private final int camouflageTcpProxyPort ;
@@ -66,12 +69,27 @@ final class Config {
6669 private final String clusterInvitePath ;
6770 private final String clusterPeerHandshakePath ;
6871 private final boolean clusterExitFallbackToDirect ;
72+ private final boolean controlPanel ;
73+ private final String controlListen ;
74+ private final int controlPort ;
75+ private final String controlDb ;
76+ private final boolean controlDoxhEnabled ;
77+ private final String controlDoxhKeyFile ;
78+ private final boolean controlAssetsProtected ;
79+ private final boolean controlWs ;
80+ private final boolean controlAudit ;
81+ private final boolean controlPublic ;
82+ private final boolean controlAllowRemote ;
83+ private final boolean genericTokenEnabled ;
84+ private final boolean genericTokenDeprecated ;
85+ private final String genericTokenDisableAfter ;
6986
7087 private Config (List <Integer > listenPorts , String token , int udpChannels , String publicHost , boolean debug ,
7188 boolean updateEnabled , String updateRepo , int updateCheckIntervalMinutes , int updateRestartExitCode ,
7289 String serverMode , int quicListenPort , String quicCertPath , String quicKeyPath ,
7390 String quicAlpn , int quicMaxStreams , int quicMaxHandshakes , int quicIdleTimeoutMs ,
7491 int quicHandshakeTimeoutMs , int quicTcpConnectTimeoutMs , int quicIngressRingSlots ,
92+ String quicCongestionControl , int quicInitialCwndPackets , long quicStreamWindowBytes ,
7593 boolean quicTraceLog , boolean tokenAutoGenerated ,
7694 boolean camouflageTcpEnabled , String camouflageTcpProxyHost , int camouflageTcpProxyPort , String camouflageHttpServerName ,
7795 boolean peerRelayEnabled , boolean quicRetryTokens , int relayMaxPerRemote , int relayMaxTotal , int relayMaxBudgetKbps ,
@@ -80,7 +98,11 @@ private Config(List<Integer> listenPorts, String token, int udpChannels, String
8098 String clusterNodeId , int clusterListen , List <String > clusterPeers , int clusterGossipIntervalMs , String clusterMapPath ,
8199 String clusterSessionsPath , String clusterClientsPath , boolean clusterHttpAuth , String clusterHttpSecret ,
82100 String clusterInvitePath , String clusterPeerHandshakePath ,
83- boolean clusterExitFallbackToDirect ) {
101+ boolean clusterExitFallbackToDirect ,
102+ boolean controlPanel , String controlListen , int controlPort , String controlDb ,
103+ boolean controlDoxhEnabled , String controlDoxhKeyFile , boolean controlAssetsProtected ,
104+ boolean controlWs , boolean controlAudit , boolean controlPublic , boolean controlAllowRemote ,
105+ boolean genericTokenEnabled , boolean genericTokenDeprecated , String genericTokenDisableAfter ) {
84106 this .listenPorts = listenPorts ;
85107 this .token = token ;
86108 this .udpChannels = udpChannels ;
@@ -101,6 +123,10 @@ private Config(List<Integer> listenPorts, String token, int udpChannels, String
101123 this .quicHandshakeTimeoutMs = quicHandshakeTimeoutMs ;
102124 this .quicTcpConnectTimeoutMs = quicTcpConnectTimeoutMs ;
103125 this .quicIngressRingSlots = quicIngressRingSlots ;
126+ this .quicCongestionControl = quicCongestionControl != null && !quicCongestionControl .isBlank ()
127+ ? quicCongestionControl .trim ().toLowerCase () : "bbr" ;
128+ this .quicInitialCwndPackets = Math .max (0 , quicInitialCwndPackets );
129+ this .quicStreamWindowBytes = Math .max (0L , quicStreamWindowBytes );
104130 this .quicTraceLog = quicTraceLog ;
105131 this .tokenAutoGenerated = tokenAutoGenerated ;
106132 this .camouflageTcpEnabled = camouflageTcpEnabled ;
@@ -132,6 +158,20 @@ private Config(List<Integer> listenPorts, String token, int udpChannels, String
132158 this .clusterInvitePath = clusterInvitePath != null && !clusterInvitePath .isBlank () ? clusterInvitePath .trim () : "/volter/cluster-invite" ;
133159 this .clusterPeerHandshakePath = clusterPeerHandshakePath != null && !clusterPeerHandshakePath .isBlank () ? clusterPeerHandshakePath .trim () : "/volter/cluster-peer-handshake" ;
134160 this .clusterExitFallbackToDirect = clusterExitFallbackToDirect ;
161+ this .controlPanel = controlPanel ;
162+ this .controlListen = controlListen != null && !controlListen .isBlank () ? controlListen .trim () : "127.0.0.1" ;
163+ this .controlPort = controlPort ;
164+ this .controlDb = controlDb != null && !controlDb .isBlank () ? controlDb .trim () : "volter-control.sqlite" ;
165+ this .controlDoxhEnabled = controlDoxhEnabled ;
166+ this .controlDoxhKeyFile = controlDoxhKeyFile != null ? controlDoxhKeyFile .trim () : "" ;
167+ this .controlAssetsProtected = controlAssetsProtected ;
168+ this .controlWs = controlWs ;
169+ this .controlAudit = controlAudit ;
170+ this .controlPublic = controlPublic ;
171+ this .controlAllowRemote = controlAllowRemote ;
172+ this .genericTokenEnabled = genericTokenEnabled ;
173+ this .genericTokenDeprecated = genericTokenDeprecated ;
174+ this .genericTokenDisableAfter = genericTokenDisableAfter != null ? genericTokenDisableAfter .trim () : "" ;
135175 }
136176
137177 String serverMode () { return serverMode ; }
@@ -145,6 +185,9 @@ private Config(List<Integer> listenPorts, String token, int udpChannels, String
145185 int quicHandshakeTimeoutMs () { return quicHandshakeTimeoutMs ; }
146186 int quicTcpConnectTimeoutMs () { return quicTcpConnectTimeoutMs ; }
147187 int quicIngressRingSlots () { return quicIngressRingSlots ; }
188+ String quicCongestionControl () { return quicCongestionControl ; }
189+ int quicInitialCwndPackets () { return quicInitialCwndPackets ; }
190+ long quicStreamWindowBytes () { return quicStreamWindowBytes ; }
148191 boolean quicTraceLog () { return quicTraceLog ; }
149192 boolean tokenAutoGenerated () { return tokenAutoGenerated ; }
150193 boolean camouflageTcpEnabled () { return camouflageTcpEnabled ; }
@@ -288,6 +331,21 @@ String publicHost() {
288331 return publicHost ;
289332 }
290333
334+ boolean controlPanel () { return controlPanel ; }
335+ String controlListen () { return controlListen ; }
336+ int controlPort () { return controlPort ; }
337+ String controlDb () { return controlDb ; }
338+ boolean controlDoxhEnabled () { return controlDoxhEnabled ; }
339+ String controlDoxhKeyFile () { return controlDoxhKeyFile ; }
340+ boolean controlAssetsProtected () { return controlAssetsProtected ; }
341+ boolean controlWs () { return controlWs ; }
342+ boolean controlAudit () { return controlAudit ; }
343+ boolean controlPublic () { return controlPublic ; }
344+ boolean controlAllowRemote () { return controlAllowRemote ; }
345+ boolean genericTokenEnabled () { return genericTokenEnabled ; }
346+ boolean genericTokenDeprecated () { return genericTokenDeprecated ; }
347+ String genericTokenDisableAfter () { return genericTokenDisableAfter ; }
348+
291349 static Config load (Path configPath ) throws IOException {
292350 Properties p = new Properties ();
293351 try (InputStream in = Files .newInputStream (configPath )) {
@@ -335,6 +393,9 @@ static Config load(Path configPath) throws IOException {
335393 int quicHandshakeTimeoutMs = Math .max (0 , parseInt (p .getProperty ("quicHandshakeTimeoutMs" ), 60_000 ));
336394 int quicTcpConnectTimeoutMs = Math .max (1_000 , parseInt (p .getProperty ("quicTcpConnectTimeoutMs" ), 10_000 ));
337395 int quicIngressRingSlots = Math .max (64 , parseInt (p .getProperty ("quicIngressRingSlots" ), 512 ));
396+ String quicCongestionControl = p .getProperty ("quicCongestionControl" , "bbr" ).trim ().toLowerCase ();
397+ int quicInitialCwndPackets = Math .max (0 , parseInt (p .getProperty ("quicInitialCwndPackets" ), 0 ));
398+ long quicStreamWindowBytes = Math .max (0L , parseLong (p .getProperty ("quicStreamWindowBytes" ), 0L ));
338399 boolean quicTraceLog = "true" .equalsIgnoreCase (p .getProperty ("quicTraceLog" , "" ).trim ());
339400 boolean camouflageTcpEnabled = "true" .equalsIgnoreCase (p .getProperty ("camouflageTcpEnabled" , "true" ).trim ());
340401 String camouflageTcpProxyHost = firstNonEmpty (p .getProperty ("camouflageTcpProxyHost" ), "127.0.0.1" );
@@ -390,6 +451,31 @@ static Config load(Path configPath) throws IOException {
390451 }
391452 boolean clusterExitFallbackToDirect =
392453 !"false" .equalsIgnoreCase (p .getProperty ("cluster.exitFallbackToDirect" , "true" ).trim ());
454+ boolean controlPanel = "true" .equalsIgnoreCase (p .getProperty ("control.panel" , "false" ).trim ());
455+ String controlListen = p .getProperty ("control.listen" , "127.0.0.1" ).trim ();
456+ int controlPort = parseInt (p .getProperty ("control.port" ), 0 );
457+ String controlDb = p .getProperty ("control.db" , "volter-control.sqlite" ).trim ();
458+ boolean controlDoxhEnabled = !"false" .equalsIgnoreCase (p .getProperty ("control.doxh.enabled" , "true" ).trim ());
459+ String controlDoxhKeyFile = p .getProperty ("control.doxh.keyFile" , "control.doxh" ).trim ();
460+ boolean controlAssetsProtected = !"false" .equalsIgnoreCase (p .getProperty ("control.assets.protected" , "true" ).trim ());
461+ boolean controlWs = !"false" .equalsIgnoreCase (p .getProperty ("control.ws" , "true" ).trim ());
462+ boolean controlAudit = !"false" .equalsIgnoreCase (p .getProperty ("control.audit" , "true" ).trim ());
463+ boolean controlPublic = "true" .equalsIgnoreCase (p .getProperty ("control.public" , "false" ).trim ());
464+ boolean controlAllowRemote = "true" .equalsIgnoreCase (p .getProperty ("control.allowRemote" , "false" ).trim ());
465+ boolean genericTokenEnabled = !"false" .equalsIgnoreCase (p .getProperty ("genericToken.enabled" , "true" ).trim ());
466+ boolean genericTokenDeprecated = !"false" .equalsIgnoreCase (p .getProperty ("genericToken.deprecated" , "true" ).trim ());
467+ String genericTokenDisableAfter = p .getProperty ("genericToken.disableAfter" , "" ).trim ();
468+ if (controlPanel ) {
469+ if (controlPort < 1 || controlPort > 65535 ) throw new IOException ("bad control.port" );
470+ if (!controlAllowRemote && !isLocalListen (controlListen )) {
471+ throw new IOException ("control.listen must be local unless control.allowRemote=true" );
472+ }
473+ if (controlPublic && !controlAllowRemote ) {
474+ throw new IOException ("control.public=true requires control.allowRemote=true" );
475+ }
476+ if (controlDb .isBlank ()) throw new IOException ("control.db is empty" );
477+ if (controlDoxhEnabled && controlDoxhKeyFile .isBlank ()) throw new IOException ("control.doxh.keyFile is empty" );
478+ }
393479 if (serverMode .equals ("quic-only" ) || serverMode .equals ("both" )) {
394480 if (quicListenPort < 1 || quicListenPort > 65535 ) throw new IOException ("bad quicListenPort" );
395481 if (quicCertPath == null || quicCertPath .isBlank ()) throw new IOException ("quicCertPath is required" );
@@ -399,6 +485,7 @@ static Config load(Path configPath) throws IOException {
399485 updateEnabled , updateRepo , updateCheckIntervalMinutes , updateRestartExitCode ,
400486 serverMode , quicListenPort , quicCertPath , quicKeyPath , quicAlpn , quicMaxStreams , quicMaxHandshakes , quicIdleTimeoutMs ,
401487 quicHandshakeTimeoutMs , quicTcpConnectTimeoutMs , quicIngressRingSlots ,
488+ quicCongestionControl , quicInitialCwndPackets , quicStreamWindowBytes ,
402489 quicTraceLog , tokenAutoGenerated ,
403490 camouflageTcpEnabled , camouflageTcpProxyHost , camouflageTcpProxyPort , camouflageHttpServerName ,
404491 peerRelayEnabled , quicRetryTokens , relayMaxPerRemote , relayMaxTotal , relayMaxBudgetKbps ,
@@ -421,7 +508,26 @@ static Config load(Path configPath) throws IOException {
421508 clusterHttpSecret ,
422509 clusterInvitePath ,
423510 clusterPeerHandshakePath ,
424- clusterExitFallbackToDirect );
511+ clusterExitFallbackToDirect ,
512+ controlPanel ,
513+ controlListen ,
514+ controlPort ,
515+ controlDb ,
516+ controlDoxhEnabled ,
517+ controlDoxhKeyFile ,
518+ controlAssetsProtected ,
519+ controlWs ,
520+ controlAudit ,
521+ controlPublic ,
522+ controlAllowRemote ,
523+ genericTokenEnabled ,
524+ genericTokenDeprecated ,
525+ genericTokenDisableAfter );
526+ }
527+
528+ private static boolean isLocalListen (String listen ) {
529+ String v = listen == null ? "" : listen .trim ().toLowerCase ();
530+ return v .isEmpty () || v .equals ("127.0.0.1" ) || v .equals ("localhost" ) || v .equals ("::1" );
425531 }
426532
427533 private static String firstNonEmpty (String a , String b ) {
@@ -439,6 +545,15 @@ private static int parseInt(String s, int def) throws IOException {
439545 }
440546 }
441547
548+ private static long parseLong (String s , long def ) throws IOException {
549+ if (s == null || s .isBlank ()) return def ;
550+ try {
551+ return Long .parseLong (s .trim ());
552+ } catch (NumberFormatException e ) {
553+ throw new IOException ("bad long: " + s , e );
554+ }
555+ }
556+
442557 private static List <Integer > parsePortsCsv (String s ) throws IOException {
443558 String [] parts = s .split ("," );
444559 List <Integer > out = new ArrayList <>();
0 commit comments