Skip to content

Commit 40a13f0

Browse files
committed
meow update
1 parent 5b0982e commit 40a13f0

16 files changed

Lines changed: 1419 additions & 161 deletions

File tree

android/app/src/main/java/dev/c0redev/volter/ui/components/ShaderBorder.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import androidx.compose.ui.composed
1313
import androidx.compose.ui.draw.drawWithCache
1414
import androidx.compose.ui.graphics.ShaderBrush
1515

16-
// бегущий зелёный курсор по периметру рамки, stable-индикатор активного соединения
1716
private const val RUNNING_BORDER_SRC = """
1817
uniform float2 u_res;
1918
uniform float u_time;
@@ -67,14 +66,12 @@ half4 main(float2 frag) {
6766
}
6867
"""
6968

70-
// рисует анимированную рамку поверх контента, пока active == true
7169
fun Modifier.runningBorder(active: Boolean): Modifier = composed {
7270
if (!active) return@composed this
7371

7472
val shader = remember { RuntimeShader(RUNNING_BORDER_SRC) }
7573
val brush = remember { ShaderBrush(shader) }
7674
val transition = rememberInfiniteTransition(label = "runningBorder")
77-
// время в секундах, период кратен 2с (циклу курсора), шов незаметен
7875
val time by transition.animateFloat(
7976
initialValue = 0f,
8077
targetValue = 120f,

server/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
<artifactId>netty-handler</artifactId>
3333
<version>${netty.version}</version>
3434
</dependency>
35+
<dependency>
36+
<groupId>io.netty</groupId>
37+
<artifactId>netty-transport-native-epoll</artifactId>
38+
<version>${netty.version}</version>
39+
<classifier>linux-x86_64</classifier>
40+
</dependency>
3541
<dependency>
3642
<groupId>io.netty.incubator</groupId>
3743
<artifactId>netty-incubator-codec-native-quic</artifactId>

server/src/main/java/dev/c0redev/volter/AeadStream.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ private synchronized void writeFrame(OutputStream out, byte[] b, int off, int n)
8686
byte[] lp = new byte[] {(byte) (encData.length >> 8), (byte) encData.length};
8787
byte[] encLen = Dexote.seal(keys.txLenKey, nonce12(txNL++), null, lp);
8888

89-
out.write(encLen);
90-
out.write(encData);
91-
out.flush();
89+
// один write на фрейм: для QUIC-sink это одна датаграмма вместо двух
90+
byte[] frame = new byte[encLen.length + encData.length];
91+
System.arraycopy(encLen, 0, frame, 0, encLen.length);
92+
System.arraycopy(encData, 0, frame, encLen.length, encData.length);
93+
out.write(frame);
9294
}
9395

9496
InputStream wrapInput(InputStream in) {

server/src/main/java/dev/c0redev/volter/ClusterRuntime.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,7 @@ private static String normalizeEndpoint(String raw) {
529529
}
530530
}
531531

532-
533-
int mergeFromJson(String raw) {
532+
private int mergeFromJson(String raw) {
534533
if (raw == null || raw.isBlank()) return 0;
535534
List<Map<String, String>> parsed = parseNodes(raw);
536535
long now = System.currentTimeMillis();

server/src/main/java/dev/c0redev/volter/Config.java

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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<>();
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package dev.c0redev.volter;
2+
3+
import java.io.IOException;
4+
import java.nio.charset.StandardCharsets;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.security.MessageDigest;
8+
import java.security.SecureRandom;
9+
import java.util.Base64;
10+
import java.util.Locale;
11+
import java.util.Map;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
14+
final class ControlAuth {
15+
private static final SecureRandom RND = new SecureRandom();
16+
private static final long SESSION_TTL_MS = 6L * 60L * 60L * 1000L;
17+
private static final String PREFIX = "sha256:";
18+
19+
private final Path keyFile;
20+
private final String keyHash;
21+
private final Map<String, Long> sessions = new ConcurrentHashMap<>();
22+
23+
private ControlAuth(Path keyFile, String keyHash) {
24+
this.keyFile = keyFile;
25+
this.keyHash = keyHash;
26+
}
27+
28+
static ControlAuth load(Path keyFile) throws IOException {
29+
Path parent = keyFile.toAbsolutePath().normalize().getParent();
30+
if (parent != null) Files.createDirectories(parent);
31+
if (!Files.exists(keyFile)) {
32+
String key = randomSecret(32);
33+
String hash = hashKey(key);
34+
Files.writeString(keyFile, PREFIX + hash + System.lineSeparator(), StandardCharsets.UTF_8);
35+
Log.logger(ControlAuth.class).warning("Generated control Doxh key. Save it now: " + key);
36+
return new ControlAuth(keyFile, hash);
37+
}
38+
String raw = Files.readString(keyFile, StandardCharsets.UTF_8).trim();
39+
if (raw.startsWith(PREFIX)) {
40+
String h = raw.substring(PREFIX.length()).trim();
41+
if (h.length() != 64) throw new IOException("bad control Doxh hash");
42+
return new ControlAuth(keyFile, h);
43+
}
44+
if (!strongKey(raw)) {
45+
throw new IOException("weak control Doxh key, use random 32+ chars secret");
46+
}
47+
String hash = hashKey(raw);
48+
Files.writeString(keyFile, PREFIX + hash + System.lineSeparator(), StandardCharsets.UTF_8);
49+
return new ControlAuth(keyFile, hash);
50+
}
51+
52+
boolean verifyKey(String key) {
53+
if (!strongKey(key)) return false;
54+
String h = hashKey(key.trim());
55+
return MessageDigest.isEqual(h.getBytes(StandardCharsets.UTF_8), keyHash.getBytes(StandardCharsets.UTF_8));
56+
}
57+
58+
String createSession() {
59+
String sid = randomSecret(32);
60+
sessions.put(sid, System.currentTimeMillis() + SESSION_TTL_MS);
61+
return sid;
62+
}
63+
64+
boolean validSession(String sid) {
65+
if (sid == null || sid.isBlank()) return false;
66+
Long exp = sessions.get(sid);
67+
if (exp == null) return false;
68+
if (exp < System.currentTimeMillis()) {
69+
sessions.remove(sid);
70+
return false;
71+
}
72+
return true;
73+
}
74+
75+
void removeSession(String sid) {
76+
if (sid != null) sessions.remove(sid);
77+
}
78+
79+
Path keyFile() { return keyFile; }
80+
81+
static boolean strongKey(String raw) {
82+
if (raw == null) return false;
83+
String s = raw.trim();
84+
if (s.length() < 32) return false;
85+
String low = s.toLowerCase(Locale.ROOT);
86+
if (low.equals("admin") || low.equals("adminadmin") || low.equals("admin:admin") ||
87+
low.equals("password") || low.equals("123456") || low.equals("qwerty") ||
88+
low.equals("volter") || low.equals("doxh")) return false;
89+
int classes = 0;
90+
if (s.chars().anyMatch(Character::isLowerCase)) classes++;
91+
if (s.chars().anyMatch(Character::isUpperCase)) classes++;
92+
if (s.chars().anyMatch(Character::isDigit)) classes++;
93+
if (s.chars().anyMatch(c -> !Character.isLetterOrDigit(c))) classes++;
94+
long distinct = s.chars().distinct().count();
95+
return classes >= 2 && distinct >= 12;
96+
}
97+
98+
private static String hashKey(String key) {
99+
try {
100+
MessageDigest md = MessageDigest.getInstance("SHA-256");
101+
byte[] b = md.digest(key.getBytes(StandardCharsets.UTF_8));
102+
StringBuilder sb = new StringBuilder(b.length * 2);
103+
for (byte x : b) sb.append(String.format("%02x", x & 0xff));
104+
return sb.toString();
105+
} catch (Exception e) {
106+
throw new IllegalStateException(e);
107+
}
108+
}
109+
110+
private static String randomSecret(int bytes) {
111+
byte[] b = new byte[bytes];
112+
RND.nextBytes(b);
113+
return Base64.getUrlEncoder().withoutPadding().encodeToString(b);
114+
}
115+
}

0 commit comments

Comments
 (0)