Minamp 1.4: faster connecting

- Remember the connection that worked and try it alone first next start
- Prefer plain HTTP on the home network (each TLS handshake costs seconds on a
  600 MHz phone); new setting "Encrypt at home too" to keep HTTPS
- Try secure addresses one at a time instead of in parallel, so they don't
  all time out together
- TLS 1.2 with session resumption: reconnects skip the full handshake
- Cache certificate chains already verified; changing the trusted roots
  clears that cache and all TLS sessions
- README: updated connection order; test: session resumption check and
  HandshakeProfile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 21:16:09 +02:00
1 parent 58d21b6d68
commit 6fd17bc9c5
9 files changed
+284 -67

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
import java.io.File;
import java.util.*;
import com.minamp.net.*;
/** Samples the handshake thread's stack to see where TLS time goes (run with -Xint). */
public class HandshakeProfile {
public static void main(String[] a) throws Exception {
CertVerifier.setRootStore(new PlexTest.RootDir(new File(a[0])));
final String url = a[1];
final int rounds = Integer.parseInt(a[2]);
final Map<String, Integer> buckets = new TreeMap<String, Integer>();
final long[] times = new long[rounds];
final Thread[] worker = new Thread[1];
worker[0] = Threads.create(new Runnable() { public void run() {
for (int i = 0; i < rounds; i++) {
Http.evictAll();
long t0 = System.nanoTime();
try { Http.get(url, null, 60000).readAll(); } catch (Exception e) { System.out.println(e); }
times[i] = (System.nanoTime() - t0) / 1000000;
}
}}, "hs");
worker[0].start();
while (worker[0].isAlive()) {
StackTraceElement[] st = worker[0].getStackTrace();
String b = "other";
for (StackTraceElement e : st) {
String c = e.getClassName() + "." + e.getMethodName();
if (c.startsWith("com.minamp.net.CertVerifier.signatureValid")) { b = "our chain check: signature math"; break; }
if (c.startsWith("com.minamp.net.CertVerifier")) { b = "our chain check: other"; break; }
if (c.contains("Ed25519") || c.contains("X25519") || c.contains("rfc7748")) { b = "X25519 key exchange"; break; }
if (c.contains("ECDSASigner") || c.contains("DSADigestSigner")) { b = "ECDSA verify (server signature)"; break; }
if (c.contains("RSA")) { b = "RSA"; break; }
if (c.contains("math.ec")) { b = "EC math (other)"; break; }
if (c.contains("asn1")) { b = "ASN.1 parsing"; break; }
if (c.contains("ChaCha") || c.contains("Poly1305") || c.contains("AES") || c.contains("GCM")) { b = "symmetric cipher"; break; }
if (c.contains("Digest") || c.contains("HMAC") || c.contains("HKDF")) { b = "hashing/HKDF"; break; }
if (c.contains("SecureRandom") || c.contains("Random")) { b = "random"; break; }
if (c.contains("java.net") || c.contains("SocketInputStream") || c.contains("NioSocket") || c.contains("read0")) { b = "network wait"; break; }
}
Integer n = buckets.get(b); buckets.put(b, n == null ? 1 : n + 1);
Thread.sleep(3);
}
int total = 0; for (int v : buckets.values()) total += v;
System.out.println("handshake ms: " + Arrays.toString(times));
List<Map.Entry<String, Integer>> l = new ArrayList<Map.Entry<String, Integer>>(buckets.entrySet());
Collections.sort(l, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> x, Map.Entry<String, Integer> y) { return y.getValue() - x.getValue(); } });
for (Map.Entry<String, Integer> e : l) System.out.printf(" %5.1f%% %s%n", 100.0 * e.getValue() / total, e.getKey());
}
}
+39
View File
@@ -196,6 +196,45 @@ public class PlexTest {
Server.Conn c3 = s3.pick();
check(c3.kind == Server.LOCAL_HTTP, "falls back when TLS name check fails -> " + c3.uri);
// --- TLS 1.2 session resumption: the second connection must skip the full handshake
com.minamp.net.DiagLog.clear();
Http.evictAll();
Thread.sleep(300);
Http.get("https://127-0-0-1.abc123.plex.direct:32443/identity", null, 10000).readAll();
Http.evictAll();
Thread.sleep(300);
long tr = System.currentTimeMillis();
Http.get("https://127-0-0-1.abc123.plex.direct:32443/identity", null, 10000).readAll();
tr = System.currentTimeMillis() - tr;
String dl = com.minamp.net.DiagLog.dump();
check(dl.indexOf("TLS resumed") >= 0, "second TLS connection resumed the session (" + tr + "ms)");
// --- picking: last good first, and a dead preferred address doesn't block a working one
Server s5 = new Server();
s5.machineId = "machine-abc123"; s5.token = s.token; s5.name = "x";
s5.add("https://10-255-255-1.abc123.plex.direct:9", Server.LOCAL); // unreachable
s5.add("http://127.0.0.1:32400", Server.LOCAL_HTTP);
s5.add("https://127-0-0-1.abc123.plex.direct:32443", Server.REMOTE);
s5.sortConnections();
long tp = System.currentTimeMillis();
Server.Conn c5 = s5.pick();
tp = System.currentTimeMillis() - tp;
check(c5.kind == Server.LOCAL_HTTP && tp < 3000, "plain home address chosen straight away (" + tp + "ms), not stuck behind HTTPS");
Server s6 = Server.fromJson(Json.parseObj(s5.toJson().toString()));
check(s6.lastGoodUri.equals("http://127.0.0.1:32400"), "working address remembered across restarts");
s6.lastGoodUri = "https://127-0-0-1.abc123.plex.direct:32443";
com.minamp.net.DiagLog.clear();
Server.Conn c6 = s6.pick();
check(c6.kind == Server.REMOTE && com.minamp.net.DiagLog.dump().indexOf("127.0.0.1:32400") < 0, "last good connection tried first, nothing else probed");
Server s7 = new Server();
s7.machineId = "machine-abc123"; s7.token = s.token; s7.name = "x";
s7.add("http://10.255.255.1:32400", Server.LOCAL_HTTP); // away from home: LAN dead
s7.add("https://127-0-0-1.abc123.plex.direct:32443", Server.REMOTE);
s7.add("https://127-0-0-1.abc123.plex.direct:32443/relay", Server.RELAY);
s7.sortConnections();
Server.Conn c7 = s7.pick();
check(c7.kind == Server.REMOTE, "away from home: falls through to remote (" + Server.kindLabel(c7.kind) + ")");
// --- manual LAN address without a token (server allows this network without sign-in)
Server lan = new Server();
lan.name = "manual"; lan.token = "";