Mult Port + TCP Calibration (SSH DEAD)

This commit is contained in:
2026-08-17 17:08:57 -03:00
parent 7ea221a99c
commit b997294607
58 changed files with 6033 additions and 497 deletions
@@ -34,14 +34,16 @@ public class DragonService extends VpnService {
public static final String ACTION_STATE = "com.dragontcp.client.STATE";
public static final String EXTRA_ACTIVE = "active";
public static final String EXTRA_STATUS = "status";
public static final String EXTRA_UP_CHUNK = "upChunk";
public static final String EXTRA_DOWN_CHUNK = "downChunk";
public static final String EXTRA_SERVER = "server";
public static final String EXTRA_PORT = "port";
public static final String EXTRA_PORT_START = "portStart";
public static final String EXTRA_PORT_END = "portEnd";
public static final String EXTRA_WORKING_PORT = "workingPort";
public static final String EXTRA_TOKEN = "token";
/** Wire format: "auto", "b", "bp" or "x". */
public static final String EXTRA_WIRE = "wire";
public static final String EXTRA_CHUNK_MAX = "chunkMax";
public static final String EXTRA_CHUNK_MIN = "chunkMin";
/** Maximum download records per request. A transport setting, not a thread count. */
public static final String EXTRA_BATCH_MAX = "batchMax";
/** Minimum download records per request; equal to the maximum pins the depth. */
@@ -52,17 +54,27 @@ public class DragonService extends VpnService {
public static final String EXTRA_PROBE_THREADS = "probeThreads";
private static final int BATCH_LIMIT = 256;
private static final int CHUNK_MIN = 32;
private static final int CHUNK_MAX = 1024 * 1024;
private static final int NOTIFICATION_ID = 53;
private static final String CHANNEL_ID = "dragontcp-lite";
private static final int LOCAL_PROXY_PORT = 8080;
private static final int TUN_MTU = 1400;
private static final int RUNTIME_SHRINK_AFTER = 1;
private static final int RUNTIME_SHRINK_STEP = 200;
private static volatile boolean appActive;
private static volatile String appStatus = "DISCONNECTED";
private static volatile int appUpChunk;
private static volatile int appDownChunk;
private static volatile int appWorkingPort;
public static boolean isActive() { return appActive; }
public static String currentStatus() { return appStatus; }
public static int currentUpChunk() { return appUpChunk; }
public static int currentDownChunk() { return appDownChunk; }
public static int currentWorkingPort() { return appWorkingPort; }
private final Object stateLock = new Object();
private volatile Process coreProcess;
@@ -96,6 +108,9 @@ public class DragonService extends VpnService {
return START_STICKY;
}
cancelRequested = false;
appUpChunk = 0;
appDownChunk = 0;
appWorkingPort = 0;
publishState("CONNECTING", true);
}
startForeground(NOTIFICATION_ID, buildNotification("Starting..."));
@@ -112,16 +127,17 @@ public class DragonService extends VpnService {
}
String server = intent.getStringExtra(EXTRA_SERVER);
int port = intent.getIntExtra(EXTRA_PORT, 53);
int portStart = intent.getIntExtra(EXTRA_PORT_START, 53);
int portEnd = intent.getIntExtra(EXTRA_PORT_END, portStart);
String token = intent.getStringExtra(EXTRA_TOKEN);
String wire = intent.getStringExtra(EXTRA_WIRE);
int chunkMax = intent.getIntExtra(EXTRA_CHUNK_MAX, 1024 * 1024);
int chunkMin = intent.getIntExtra(EXTRA_CHUNK_MIN, 32);
int chunkMax = CHUNK_MAX;
int chunkMin = CHUNK_MIN;
int batchMax = intent.getIntExtra(EXTRA_BATCH_MAX, 1);
int batchMin = intent.getIntExtra(EXTRA_BATCH_MIN, 1);
int reconnect = intent.getIntExtra(EXTRA_RECONNECT, 1);
int timeout = intent.getIntExtra(EXTRA_TIMEOUT, 5);
int probeDelay = intent.getIntExtra(EXTRA_PROBE_DELAY, 1000);
int probeDelay = intent.getIntExtra(EXTRA_PROBE_DELAY, 100);
int probeThreads = intent.getIntExtra(EXTRA_PROBE_THREADS, 1);
if (server == null || server.trim().isEmpty()) {
@@ -131,27 +147,36 @@ public class DragonService extends VpnService {
server = server.trim();
if (token == null) token = "";
if (wire == null || !(wire.equals("b") || wire.equals("bp") || wire.equals("x"))) wire = "auto";
chunkMax = Math.max(32, Math.min(1024 * 1024, chunkMax));
chunkMin = Math.max(32, Math.min(chunkMax, chunkMin));
batchMax = Math.max(1, Math.min(BATCH_LIMIT, batchMax));
batchMin = Math.max(1, Math.min(batchMax, batchMin));
reconnect = Math.max(0, reconnect);
timeout = Math.max(1, timeout);
probeDelay = Math.max(200, Math.min(30000, probeDelay));
probeDelay = Math.max(50, Math.min(30000, probeDelay));
probeThreads = Math.max(1, Math.min(16, probeThreads));
if (portStart < 1 || portStart > 65535 || portEnd < 1 || portEnd > 65535 || portStart > portEnd) {
failStart("Invalid server port range");
return;
}
try {
AppLog.append("Starting DragonTCP → " + server + ":" + port);
AppLog.append("Starting DragonTCP → " + server + " ports " + portStart + "-" + portEnd);
AppLog.append("Port scan: ascending " + portStart + "-" + portEnd + " • one candidate at a time • real DragonTCP validation");
AppLog.append(describeBatch(batchMin, batchMax));
AppLog.append(describeWire(wire));
AppLog.append("Wire probe: http://ip.dr2.site/ • delay " + probeDelay + " ms • threads " + probeThreads);
Process process = startDragonCore(server, port, token, chunkMax, chunkMin, batchMax, batchMin, wire, reconnect, timeout, probeDelay, probeThreads);
AppLog.append("Payload: SHA-256 masked framing (clear-payload mode disabled in Android)");
AppLog.append("Chunk startup: ASCENDING calibration from " + chunkMin + " to " + chunkMax
+ " bytes; 4x growth then ~32-byte boundary refinement");
AppLog.append("Runtime chunk recovery: reduce immediately by " + RUNTIME_SHRINK_STEP
+ " bytes after a recoverable transfer failure; I/O timeout kills the connection instead");
AppLog.append("Header scan: valid masks only • server-local probe • delay " + probeDelay + " ms • threads " + probeThreads);
AppLog.append("SSH: disabled in Android app (server/core SSH support retained)");
Process process = startDragonCore(server, portStart, portEnd, token, chunkMax, chunkMin, batchMax, batchMin, wire,
reconnect, timeout, probeDelay, probeThreads);
synchronized (stateLock) { coreProcess = process; }
startCoreLogReader(process);
waitForLocalProxy(process);
waitForLocalProxy(process, LOCAL_PROXY_PORT);
if (cancelRequested) throw new InterruptedException("Stop requested");
AppLog.append("Local DragonTCP proxy ready on 127.0.0.1:8080");
AppLog.append("Local DragonTCP proxy ready on 127.0.0.1:" + LOCAL_PROXY_PORT);
ParcelFileDescriptor pfd = establishVpn();
if (pfd == null) throw new IllegalStateException("Android refused to establish the VPN interface");
@@ -205,7 +230,8 @@ public class DragonService extends VpnService {
private Process startDragonCore(
String server,
int port,
int portStart,
int portEnd,
String token,
int chunkMax,
int chunkMin,
@@ -225,7 +251,8 @@ public class DragonService extends VpnService {
cmd.add("--listen-host"); cmd.add("127.0.0.1");
cmd.add("--listen-port"); cmd.add(Integer.toString(LOCAL_PROXY_PORT));
cmd.add("--server-host"); cmd.add(server);
cmd.add("--server-port"); cmd.add(Integer.toString(port));
cmd.add("--server-port-start"); cmd.add(Integer.toString(portStart));
cmd.add("--server-port-end"); cmd.add(Integer.toString(portEnd));
if (!token.isEmpty()) { cmd.add("--token"); cmd.add(token); }
cmd.add("--transport"); cmd.add("chunk");
cmd.add("--chunk-start"); cmd.add(Integer.toString(chunkMax));
@@ -241,6 +268,12 @@ public class DragonService extends VpnService {
cmd.add("--chunk-timeout"); cmd.add(timeout + "s");
cmd.add("--chunk-grow-after"); cmd.add("16");
cmd.add("--chunk-adapt-log=true");
// Android always uses ascending startup calibration. MAX-FIRST remains
// available in the Go CLI for compatibility but is intentionally never
// enabled by the app. Runtime recovery is immediate and fine-grained.
cmd.add("--chunk-max-first=false");
cmd.add("--chunk-shrink-after"); cmd.add(Integer.toString(RUNTIME_SHRINK_AFTER));
cmd.add("--chunk-shrink-step"); cmd.add(Integer.toString(RUNTIME_SHRINK_STEP));
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
@@ -271,11 +304,22 @@ public class DragonService extends VpnService {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
// Keep the UI useful: adaptation changes and real errors only.
handleCoreTelemetry(line);
String lower = line.toLowerCase();
if (line.startsWith("adaptive ") || line.startsWith("path probe:") || line.startsWith("wire") || lower.contains("error") || lower.contains("failed")) {
if (line.startsWith("[D-TCP]") || line.startsWith("adaptive ")
|| line.startsWith("path probe:") || line.startsWith("wire")
|| lower.contains("error") || lower.contains("failed")) {
AppLog.append(line);
}
if (line.startsWith("[D-TCP] phase=PORT_SCAN state=starting")) {
publishState("TESTING PORTS", true);
} else if (line.startsWith("[D-TCP] phase=AUTH state=starting")) {
publishState("AUTHENTICATING", true);
} else if (line.startsWith("[D-TCP] phase=CALIBRATION state=starting")) {
publishState("TESTING CARRIER", true);
} else if (line.startsWith("[D-TCP] phase=ACTIVE")) {
publishState("TRANSPORT READY", true);
}
}
} catch (Throwable ignored) {
}
@@ -302,13 +346,92 @@ public class DragonService extends VpnService {
watcher.start();
}
private void waitForLocalProxy(Process process) throws Exception {
long deadline = System.currentTimeMillis() + 10_000;
private void handleCoreTelemetry(String line) {
if (line == null || line.isEmpty()) return;
String lower = line.toLowerCase();
if (line.startsWith("[D-TCP] phase=PORT_SCAN") && line.contains("state=success")) {
int port = parseNamedInt(line, "port=");
if (port > 0) updateWorkingPort(port);
return;
}
if (line.startsWith("[D-TCP] phase=ACTIVE")) {
int up = parseNamedInt(line, "upload_chunk=");
int down = parseNamedInt(line, "download_chunk=");
updateLiveChunks(up, down);
return;
}
if (line.startsWith("[D-TCP] phase=CALIBRATION fake_iperf=")) {
int chunk = parseNamedInt(line, "chunk=");
if (chunk > 0) {
if (lower.contains("fake_iperf=upload")) updateLiveChunks(chunk, 0);
else if (lower.contains("fake_iperf=download")) updateLiveChunks(0, chunk);
}
return;
}
if (line.startsWith("adaptive ") && line.contains(" chunk:")) {
int arrow = line.indexOf("->");
if (arrow >= 0) {
int value = parseLeadingInt(line, arrow + 2);
if (value > 0) {
if (lower.contains("upload chunk:")) updateLiveChunks(value, 0);
else if (lower.contains("download chunk:")) updateLiveChunks(0, value);
}
}
}
}
private static int parseNamedInt(String line, String key) {
int at = line.indexOf(key);
if (at < 0) return 0;
return parseLeadingInt(line, at + key.length());
}
private static int parseLeadingInt(String line, int start) {
int i = start;
while (i < line.length() && Character.isWhitespace(line.charAt(i))) i++;
int value = 0;
boolean found = false;
while (i < line.length()) {
char c = line.charAt(i);
if (c < '0' || c > '9') break;
found = true;
value = value * 10 + (c - '0');
i++;
}
return found ? value : 0;
}
private void updateLiveChunks(int up, int down) {
boolean changed = false;
if (up > 0 && up != appUpChunk) {
appUpChunk = up;
changed = true;
}
if (down > 0 && down != appDownChunk) {
appDownChunk = down;
changed = true;
}
if (changed) publishState(appStatus, appActive);
}
private void updateWorkingPort(int port) {
if (port > 0 && port != appWorkingPort) {
appWorkingPort = port;
publishState(appStatus, appActive);
}
}
private void waitForLocalProxy(Process process, int localPort) throws Exception {
long deadline = System.currentTimeMillis() + 300_000;
Throwable last = null;
while (System.currentTimeMillis() < deadline) {
if (!process.isAlive()) throw new IllegalStateException("DragonTCP core exited before proxy startup");
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("127.0.0.1", LOCAL_PROXY_PORT), 150);
socket.connect(new InetSocketAddress("127.0.0.1", localPort), 150);
return;
} catch (Throwable t) {
last = t;
@@ -408,6 +531,9 @@ public class DragonService extends VpnService {
stopping = true;
cleanupComponentsLocked();
if (logMessage != null) AppLog.append(logMessage);
appUpChunk = 0;
appDownChunk = 0;
appWorkingPort = 0;
publishState("DISCONNECTED", false);
try { stopForeground(true); } catch (Throwable ignored) {}
stopSelf();
@@ -438,6 +564,9 @@ public class DragonService extends VpnService {
state.setPackage(getPackageName());
state.putExtra(EXTRA_ACTIVE, active);
state.putExtra(EXTRA_STATUS, appStatus);
state.putExtra(EXTRA_UP_CHUNK, appUpChunk);
state.putExtra(EXTRA_DOWN_CHUNK, appDownChunk);
state.putExtra(EXTRA_WORKING_PORT, appWorkingPort);
try { sendBroadcast(state); } catch (Throwable ignored) {}
}
@@ -48,10 +48,9 @@ public class MainActivity extends Activity {
private static final int BAD = Color.rgb(232, 116, 116);
private EditText server;
private EditText port;
private EditText portStart;
private EditText portEnd;
private EditText token;
private EditText chunkMax;
private EditText chunkMin;
private EditText batchMax;
private EditText batchMin;
private EditText reconnect;
@@ -70,6 +69,9 @@ public class MainActivity extends Activity {
private Button stopButton;
private Button logsButton;
private TextView statusChip;
private TextView upChunkValue;
private TextView downChunkValue;
private TextView workingPortValue;
private boolean receiverRegistered;
private boolean connectPending;
@@ -80,8 +82,13 @@ public class MainActivity extends Activity {
if (!DragonService.ACTION_STATE.equals(intent.getAction())) return;
boolean active = intent.getBooleanExtra(DragonService.EXTRA_ACTIVE, false);
String status = intent.getStringExtra(DragonService.EXTRA_STATUS);
int upChunk = intent.getIntExtra(DragonService.EXTRA_UP_CHUNK, DragonService.currentUpChunk());
int downChunk = intent.getIntExtra(DragonService.EXTRA_DOWN_CHUNK, DragonService.currentDownChunk());
int workingPort = intent.getIntExtra(DragonService.EXTRA_WORKING_PORT, DragonService.currentWorkingPort());
connectPending = false;
updateConnectionUi(active, status == null ? (active ? "CONNECTED" : "DISCONNECTED") : status);
updateLiveChunkUi(upChunk, downChunk);
updateWorkingPortUi(workingPort);
}
};
@@ -111,6 +118,8 @@ public class MainActivity extends Activity {
receiverRegistered = true;
}
updateConnectionUi(DragonService.isActive(), DragonService.currentStatus());
updateLiveChunkUi(DragonService.currentUpChunk(), DragonService.currentDownChunk());
updateWorkingPortUi(DragonService.currentWorkingPort());
}
@Override
@@ -216,10 +225,12 @@ public class MainActivity extends Activity {
// ---------------------------------------------------------- connection
LinearLayout connectionCard = card("CONNECTION");
server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false);
LinearLayout connectionRow = row();
port = addFieldToRow(connectionRow, "Port", "53", "53", true, false, 0.34f);
token = addFieldToRow(connectionRow, "Token", "Optional", "", false, true, 0.66f);
connectionCard.addView(connectionRow);
LinearLayout portRow = row();
portStart = addFieldToRow(portRow, "Port start", "53", "53", true, false, 0.5f);
portEnd = addFieldToRow(portRow, "Port end", "53", "53", true, false, 0.5f);
connectionCard.addView(portRow);
token = addField(connectionCard, "Token", "Optional", "", false, true);
connectionCard.addView(hint("Ports are tested from start to end. A port is accepted only after a real DragonTCP wire/header probe succeeds."));
settings.addView(connectionCard, cardParams());
// ---------------------------------------------------------------- wire
@@ -246,14 +257,65 @@ public class MainActivity extends Activity {
wireBP.setOnClickListener(v -> setWireMode("bp"));
wireX.setOnClickListener(v -> setWireMode("x"));
// --------------------------------------------------------- record size
LinearLayout sizeCard = card("RECORD SIZE (BYTES)");
LinearLayout chunks = row();
chunkMax = addFieldToRow(chunks, "Max chunk", "1048576", "1048576", true, false, 0.58f);
chunkMin = addFieldToRow(chunks, "Min chunk", "32", "32", true, false, 0.42f);
sizeCard.addView(chunks);
sizeCard.addView(hint("Probed automatically on connect, then adapted if the path changes."));
settings.addView(sizeCard, cardParams());
// ------------------------------------------------------ live chunk size
LinearLayout liveCard = card("LIVE TRANSPORT");
liveCard.addView(hint(
"Working port and UP/DW chunk sizes update directly from the DragonTCP core, even when the log screen is closed."
));
LinearLayout liveRow = row();
LinearLayout upBox = new LinearLayout(this);
upBox.setOrientation(LinearLayout.VERTICAL);
upBox.setGravity(Gravity.CENTER);
upBox.setPadding(dp(8), dp(10), dp(8), dp(10));
upBox.setBackground(roundRect(FIELD, 12, BORDER, 1));
TextView upLabel = text("UP CHUNK", 11, MUTED, true);
upLabel.setGravity(Gravity.CENTER);
upChunkValue = text("--", 18, OK, true);
upChunkValue.setGravity(Gravity.CENTER);
upChunkValue.setPadding(0, dp(4), 0, 0);
upBox.addView(upLabel);
upBox.addView(upChunkValue);
LinearLayout downBox = new LinearLayout(this);
downBox.setOrientation(LinearLayout.VERTICAL);
downBox.setGravity(Gravity.CENTER);
downBox.setPadding(dp(8), dp(10), dp(8), dp(10));
downBox.setBackground(roundRect(FIELD, 12, BORDER, 1));
TextView downLabel = text("DW CHUNK", 11, MUTED, true);
downLabel.setGravity(Gravity.CENTER);
downChunkValue = text("--", 18, ACCENT, true);
downChunkValue.setGravity(Gravity.CENTER);
downChunkValue.setPadding(0, dp(4), 0, 0);
downBox.addView(downLabel);
downBox.addView(downChunkValue);
LinearLayout portBox = new LinearLayout(this);
portBox.setOrientation(LinearLayout.VERTICAL);
portBox.setGravity(Gravity.CENTER);
portBox.setPadding(dp(8), dp(10), dp(8), dp(10));
portBox.setBackground(roundRect(FIELD, 12, BORDER, 1));
TextView portLabel = text("WORKING PORT", 11, MUTED, true);
portLabel.setGravity(Gravity.CENTER);
workingPortValue = text("--", 18, WARN, true);
workingPortValue.setGravity(Gravity.CENTER);
workingPortValue.setPadding(0, dp(4), 0, 0);
portBox.addView(portLabel);
portBox.addView(workingPortValue);
LinearLayout.LayoutParams upParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
upParams.setMarginEnd(dp(4));
LinearLayout.LayoutParams downParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
downParams.setMarginStart(dp(4));
downParams.setMarginEnd(dp(4));
LinearLayout.LayoutParams portParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
portParams.setMarginStart(dp(4));
liveRow.addView(upBox, upParams);
liveRow.addView(downBox, downParams);
liveRow.addView(portBox, portParams);
liveRow.setPadding(0, dp(8), 0, 0);
liveCard.addView(liveRow);
settings.addView(liveCard, cardParams());
// ------------------------------------------------------- download batch
LinearLayout batchCard = card("DOWNLOAD BATCH");
@@ -291,13 +353,13 @@ public class MainActivity extends Activity {
timeout = addFieldToRow(timing, "Timeout (s)", "5", "5", true, false, 0.42f);
advancedCard.addView(timing);
LinearLayout probing = row();
probeDelay = addFieldToRow(probing, "Probe delay (ms)", "1000", "1000", true, false, 0.65f);
probeDelay = addFieldToRow(probing, "Probe delay (ms)", "100", "100", true, false, 0.65f);
probeThreads = addFieldToRow(probing, "Probe threads", "1", "1", true, false, 0.35f);
advancedCard.addView(probing);
advancedCard.addView(hint(
"1 = auto (recommended: starts persistent, then learns one request per "
+ "connection only if reuse really fails) • 0 = persistent • N = rotate every N requests\n"
+ "Probe delay spaces startup profile connections; 1000 ms is recommended. "
+ "Probe delay spaces the single-poller valid-mask scan; 100 ms is recommended. "
+ "Probe threads defaults to 1; more may trigger carrier limits."));
settings.addView(advancedCard, cardParams());
@@ -323,7 +385,8 @@ public class MainActivity extends Activity {
settings.addView(logsButton, logsParams);
TextView note = text(
"IPv4 uses the Android VPN adapter. IPv6 is captured and blocked to prevent bypass. DNS is tunneled to 1.1.1.1 over DragonTCP.",
"IPv4 uses the Android VPN adapter. IPv6 is captured and blocked to prevent bypass. "
+ "Android uses direct DragonTCP mode; SSH remains in the project/server but is disabled in this app build.",
11, MUTED, false
);
note.setLineSpacing(0, 1.08f);
@@ -345,6 +408,23 @@ public class MainActivity extends Activity {
updateConnectionUi(false, "DISCONNECTED");
}
private void updateLiveChunkUi(int up, int down) {
if (upChunkValue != null) upChunkValue.setText(formatChunk(up));
if (downChunkValue != null) downChunkValue.setText(formatChunk(down));
}
private void updateWorkingPortUi(int value) {
if (workingPortValue != null) workingPortValue.setText(value > 0 ? Integer.toString(value) : "--");
}
private String formatChunk(int bytes) {
if (bytes <= 0) return "--";
if (bytes == 1024 * 1024) return "1 MiB\n1048576 B";
if (bytes >= 1024) return (bytes / 1024) + " KiB\n" + bytes + " B";
return bytes + " B";
}
/** Describes the batch configuration in words, live, as the user types. */
private void updateBatchHint() {
if (batchHint == null) return;
@@ -486,16 +566,16 @@ public class MainActivity extends Activity {
if (wireHint == null) return;
if ("auto".equals(mode)) {
wireHint.setTextColor(ACCENT);
wireHint.setText("Auto: discovers a working B/BP/X profile and keeps it until restart.");
wireHint.setText("Auto: scans only direct-valid masked headers (B/BP: 32, X: 96) and keeps the first working profile.");
} else if ("b".equals(mode)) {
wireHint.setTextColor(OK);
wireHint.setText("B only; header profile is discovered automatically.");
wireHint.setText("B only; scans the 32 valid masks: 00,08,10,...,F8.");
} else if ("bp".equals(mode)) {
wireHint.setTextColor(OK);
wireHint.setText("BP only.");
wireHint.setText("BP only; scans the 32 valid masks: 00,08,10,...,F8.");
} else {
wireHint.setTextColor(OK);
wireHint.setText("X only; header profile is discovered automatically.");
wireHint.setText("X only; scans only masks valid for the direct UP classifier (96 values).");
}
}
@@ -586,16 +666,16 @@ public class MainActivity extends Activity {
SharedPreferences p = getSharedPreferences(PREFS, MODE_PRIVATE);
Intent i = new Intent(this, DragonService.class).setAction(DragonService.ACTION_CONNECT);
i.putExtra(DragonService.EXTRA_SERVER, p.getString("server", ""));
i.putExtra(DragonService.EXTRA_PORT, p.getInt("port", 53));
int legacyPort = p.getInt("port", 53);
i.putExtra(DragonService.EXTRA_PORT_START, p.getInt("portStart", legacyPort));
i.putExtra(DragonService.EXTRA_PORT_END, p.getInt("portEnd", legacyPort));
i.putExtra(DragonService.EXTRA_TOKEN, p.getString("token", ""));
i.putExtra(DragonService.EXTRA_WIRE, p.getString("wire", "auto"));
i.putExtra(DragonService.EXTRA_CHUNK_MAX, p.getInt("max", 1048576));
i.putExtra(DragonService.EXTRA_CHUNK_MIN, p.getInt("min", 32));
i.putExtra(DragonService.EXTRA_BATCH_MAX, p.getInt("batchMax", 1));
i.putExtra(DragonService.EXTRA_BATCH_MIN, p.getInt("batchMin", 1));
i.putExtra(DragonService.EXTRA_RECONNECT, p.getInt("reconnect", 1));
i.putExtra(DragonService.EXTRA_TIMEOUT, p.getInt("timeout", 5));
i.putExtra(DragonService.EXTRA_PROBE_DELAY, p.getInt("probeDelay", 1000));
i.putExtra(DragonService.EXTRA_PROBE_DELAY, p.getInt("probeDelay", 100));
i.putExtra(DragonService.EXTRA_PROBE_THREADS, p.getInt("probeThreads", 1));
if (Build.VERSION.SDK_INT >= 26) startForegroundService(i); else startService(i);
}
@@ -603,30 +683,35 @@ public class MainActivity extends Activity {
private void validateAndSave() {
String h = server.getText().toString().trim();
if (h.isEmpty()) throw new IllegalArgumentException("Server is required");
int p = parse(port, 1, 65535, "Port");
int max = parse(chunkMax, 32, 1048576, "Max chunk");
int min = parse(chunkMin, 32, max, "Min chunk");
int pStart = parse(portStart, 1, 65535, "Port start");
int pEnd = parse(portEnd, 1, 65535, "Port end");
if (pStart > pEnd) throw new IllegalArgumentException("Port start must not exceed port end");
int bMax = parse(batchMax, 1, BATCH_LIMIT, "Batch max");
int bMin = parse(batchMin, 1, BATCH_LIMIT, "Batch min");
if (bMin > bMax) throw new IllegalArgumentException("Batch min must not exceed batch max");
int rec = parse(reconnect, 0, 1000000, "Reconnect every");
int tout = parse(timeout, 1, 120, "Timeout");
int delay = parse(probeDelay, 200, 30000, "Probe delay");
int delay = parse(probeDelay, 50, 30000, "Probe delay");
int threads = parse(probeThreads, 1, 16, "Probe threads");
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
.putString("server", h)
.putInt("port", p)
.putInt("portStart", pStart)
.putInt("portEnd", pEnd)
.putInt("port", pStart)
.putString("token", token.getText().toString())
.putString("wire", wireMode)
.putInt("max", max)
.putInt("min", min)
.putInt("batchMax", bMax)
.putInt("batchMin", bMin)
.putInt("reconnect", rec)
.putInt("timeout", tout)
.putInt("probeDelay", delay)
.putInt("probeThreads", threads)
.remove("sshUser")
.remove("sshPassword")
.remove("forceMaxFirst")
.remove("forceClearPayload")
.remove("max")
.remove("min")
.apply();
}
@@ -641,17 +726,29 @@ public class MainActivity extends Activity {
private void loadSettings() {
SharedPreferences p = getSharedPreferences(PREFS, MODE_PRIVATE);
server.setText(p.getString("server", ""));
port.setText(Integer.toString(p.getInt("port", 53)));
int legacyPort = p.getInt("port", 53);
portStart.setText(Integer.toString(p.getInt("portStart", legacyPort)));
portEnd.setText(Integer.toString(p.getInt("portEnd", legacyPort)));
token.setText(p.getString("token", ""));
setWireMode(p.getString("wire", "auto"));
chunkMax.setText(Integer.toString(p.getInt("max", 1048576)));
chunkMin.setText(Integer.toString(p.getInt("min", 32)));
batchMax.setText(Integer.toString(p.getInt("batchMax", 1)));
batchMin.setText(Integer.toString(p.getInt("batchMin", 1)));
reconnect.setText(Integer.toString(p.getInt("reconnect", 1)));
timeout.setText(Integer.toString(p.getInt("timeout", 5)));
probeDelay.setText(Integer.toString(p.getInt("probeDelay", 1000)));
probeDelay.setText(Integer.toString(p.getInt("probeDelay", 100)));
probeThreads.setText(Integer.toString(p.getInt("probeThreads", 1)));
// Remove legacy app-only SSH/MAX-FIRST preferences. SSH support remains
// in the Go project, but this Android UI intentionally never enables it.
p.edit()
.remove("sshUser")
.remove("sshPassword")
.remove("forceMaxFirst")
.remove("forceClearPayload")
.remove("max")
.remove("min")
.apply();
updateLiveChunkUi(DragonService.currentUpChunk(), DragonService.currentDownChunk());
updateWorkingPortUi(DragonService.currentWorkingPort());
}
private void updateConnectionUi(boolean active, String rawStatus) {
@@ -660,7 +757,7 @@ public class MainActivity extends Activity {
boolean connected = status.contains("CONNECTED") && !status.contains("DISCONNECTED");
boolean starting = active && !connected && !stopping;
statusChip.setText(connected ? "CONNECTED" : starting ? "CONNECTING" : stopping ? "STOPPING" : "DISCONNECTED");
statusChip.setText(connected ? "CONNECTED" : stopping ? "STOPPING" : starting ? status : "DISCONNECTED");
statusChip.setTextColor(connected ? OK : starting || stopping ? WARN : MUTED);
statusChip.setBackground(roundRect(
connected ? Color.rgb(20, 54, 42) : starting || stopping ? Color.rgb(58, 43, 22) : CARD,
@@ -1,6 +1,7 @@
package tech.xvanturing.freeproxy.vpn.proxy
import tech.xvanturing.freeproxy.data.model.ProxyProfile
import tech.xvanturing.freeproxy.data.model.ProxyType
import java.io.IOException
import java.io.InputStream
import java.net.DatagramSocket
@@ -9,12 +10,14 @@ import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
private fun Byte.u8(): Int = toInt() and 0xFF
/**
* Minimal HTTP CONNECT upstream client used by DragonTCP Lite.
* Upstream proxy client used by the userspace VPN engine.
*
* The upstream proxy is always the local DragonTCP Go process on
* 127.0.0.1:8080. DragonTCP then carries the stream over adaptive XOR-framed
* TCP/53 to the remote server.
* Direct DragonTCP mode talks HTTP CONNECT to the local Go core. SSH mode uses
* the Go core's local SOCKS5 listener; TCP CONNECT becomes SSH direct-tcpip and
* UDP ASSOCIATE is bridged to the server's BadVPN-compatible UDPGW.
*/
class ProxyClient(
private val profile: ProxyProfile,
@@ -23,16 +26,30 @@ class ProxyClient(
) {
@Throws(IOException::class)
fun connectTcp(destination: InetAddress, destinationPort: Int): Socket {
val socket = Socket()
try {
socket.bind(InetSocketAddress(0))
if (!protector.protect(socket)) {
throw IOException("Unable to protect local proxy socket from VPN")
}
socket.connect(proxyAddress, CONNECT_TIMEOUT_MS)
socket.soTimeout = HANDSHAKE_TIMEOUT_MS
socket.tcpNoDelay = true
return when (profile.type) {
ProxyType.HTTP -> connectHttp(destination, destinationPort)
ProxyType.SOCKS5 -> connectSocks(destination, destinationPort)
}
}
@Throws(IOException::class)
private fun newProtectedSocket(): Socket {
val socket = Socket()
socket.bind(InetSocketAddress(0))
if (!protector.protect(socket)) {
socket.close()
throw IOException("Unable to protect local proxy socket from VPN")
}
socket.connect(proxyAddress, CONNECT_TIMEOUT_MS)
socket.soTimeout = HANDSHAKE_TIMEOUT_MS
socket.tcpNoDelay = true
return socket
}
@Throws(IOException::class)
private fun connectHttp(destination: InetAddress, destinationPort: Int): Socket {
val socket = newProtectedSocket()
try {
val literal = if (destination is Inet6Address) {
"[${destination.hostAddress}]:$destinationPort"
} else {
@@ -60,7 +77,6 @@ class ProxyClient(
if (code !in 200..299) {
throw IOException("Local DragonTCP CONNECT failed: $status")
}
socket.soTimeout = 0
return socket
} catch (t: Throwable) {
@@ -69,16 +85,113 @@ class ProxyClient(
}
}
/** HTTP CONNECT does not support SOCKS5 UDP ASSOCIATE. */
@Throws(IOException::class)
private fun connectSocks(destination: InetAddress, destinationPort: Int): Socket {
val socket = newProtectedSocket()
try {
socksGreeting(socket)
val out = socket.getOutputStream()
out.write(byteArrayOf(SOCKS_VERSION.toByte(), CMD_CONNECT.toByte(), 0))
writeSocksAddress(out = out, address = destination, port = destinationPort)
out.flush()
readSocksReply(socket.getInputStream())
socket.soTimeout = 0
return socket
} catch (t: Throwable) {
runCatching { socket.close() }
throw t
}
}
/** Opens a SOCKS5 UDP ASSOCIATE control connection and returns its relay endpoint. */
@Throws(IOException::class)
fun openUdpAssociate(): UdpAssociation {
throw IOException("UDP ASSOCIATE unavailable with local HTTP CONNECT proxy")
if (profile.type != ProxyType.SOCKS5) {
throw IOException("UDP ASSOCIATE requires the local SOCKS5 proxy")
}
val socket = newProtectedSocket()
try {
socksGreeting(socket)
socket.getOutputStream().apply {
write(byteArrayOf(SOCKS_VERSION.toByte(), CMD_UDP_ASSOCIATE.toByte(), 0, ATYP_IPV4.toByte()))
write(byteArrayOf(0, 0, 0, 0, 0, 0))
flush()
}
val relay = readSocksReply(socket.getInputStream())
socket.soTimeout = 0
return UdpAssociation(socket, relay)
} catch (t: Throwable) {
runCatching { socket.close() }
throw t
}
}
@Throws(IOException::class)
private fun socksGreeting(socket: Socket) {
socket.getOutputStream().apply {
write(byteArrayOf(SOCKS_VERSION.toByte(), 1, METHOD_NO_AUTH.toByte()))
flush()
}
val response = socket.getInputStream().readExactly(2)
if (response[0].u8() != SOCKS_VERSION || response[1].u8() != METHOD_NO_AUTH) {
throw IOException("Local SOCKS5 proxy rejected no-auth negotiation")
}
}
@Throws(IOException::class)
private fun readSocksReply(input: InputStream): InetSocketAddress {
val header = input.readExactly(4)
if (header[0].u8() != SOCKS_VERSION) throw IOException("Invalid SOCKS5 reply version")
if (header[1].u8() != 0) throw IOException("SOCKS5 request failed: reply=${header[1].u8()}")
if (header[2].u8() != 0) throw IOException("Invalid SOCKS5 reserved byte")
val host = when (header[3].u8()) {
ATYP_IPV4 -> InetAddress.getByAddress(input.readExactly(4)).hostAddress
ATYP_IPV6 -> InetAddress.getByAddress(input.readExactly(16)).hostAddress
ATYP_DOMAIN -> {
val length = input.readExactly(1)[0].u8()
if (length == 0) throw IOException("Empty SOCKS5 relay domain")
String(input.readExactly(length), Charsets.ISO_8859_1)
}
else -> throw IOException("Unsupported SOCKS5 relay address type")
}
val portBytes = input.readExactly(2)
val port = (portBytes[0].u8() shl 8) or portBytes[1].u8()
if (port == 0) throw IOException("SOCKS5 relay returned port 0")
// RFC 1928 permits 0.0.0.0 as BND.ADDR. For a local SOCKS listener,
// the usable endpoint is then the same host we connected to.
val relayHost = if (host == "0.0.0.0" || host == "::" || host == "0:0:0:0:0:0:0:0") {
proxyAddress.address?.hostAddress ?: proxyAddress.hostString
} else host
return InetSocketAddress(relayHost, port)
}
private fun writeSocksAddress(out: java.io.OutputStream, address: InetAddress, port: Int) {
val raw = address.address
when (raw.size) {
4 -> {
out.write(ATYP_IPV4)
out.write(raw)
}
16 -> {
out.write(ATYP_IPV6)
out.write(raw)
}
else -> throw IOException("Unsupported IP address length ${raw.size}")
}
out.write((port ushr 8) and 0xFF)
out.write(port and 0xFF)
}
companion object {
const val ATYP_IPV4 = 0x01
const val ATYP_DOMAIN = 0x03
const val ATYP_IPV6 = 0x04
private const val SOCKS_VERSION = 0x05
private const val METHOD_NO_AUTH = 0x00
private const val CMD_CONNECT = 0x01
private const val CMD_UDP_ASSOCIATE = 0x03
private const val CONNECT_TIMEOUT_MS = 10_000
private const val HANDSHAKE_TIMEOUT_MS = 15_000
private const val MAX_HEADER_LINE = 8192