V7
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package com.dragontcp.client;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public final class AppLog {
|
||||
public interface Listener { void onLine(String line); }
|
||||
|
||||
private static final int MAX_LINES = 600;
|
||||
private static final ArrayDeque<String> lines = new ArrayDeque<>();
|
||||
private static final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
private AppLog() {}
|
||||
|
||||
public static void append(String line) {
|
||||
if (line == null) return;
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) return;
|
||||
synchronized (lines) {
|
||||
while (lines.size() >= MAX_LINES) lines.removeFirst();
|
||||
lines.addLast(line);
|
||||
}
|
||||
for (Listener listener : listeners) {
|
||||
try { listener.onLine(line); } catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public static String history() {
|
||||
StringBuilder out = new StringBuilder();
|
||||
synchronized (lines) {
|
||||
for (String line : lines) out.append(line).append('\n');
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
synchronized (lines) { lines.clear(); }
|
||||
}
|
||||
|
||||
public static void addListener(Listener listener) { listeners.addIfAbsent(listener); }
|
||||
public static void removeListener(Listener listener) { listeners.remove(listener); }
|
||||
}
|
||||
@@ -6,101 +6,376 @@ import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DragonService extends VpnService {
|
||||
public static final String ACTION_CONNECT="com.dragontcp.client.CONNECT";
|
||||
public static final String ACTION_STOP="com.dragontcp.client.STOP";
|
||||
public static volatile boolean active=false,running=false;
|
||||
public static volatile String state="Stopped";
|
||||
private static final String CHANNEL_ID="dragontcp_vpn";
|
||||
private static final int NOTIFICATION_ID=53;
|
||||
private final Object lifecycleLock=new Object();
|
||||
private Process process;
|
||||
private Thread outputThread;
|
||||
private ParcelFileDescriptor vpnInterface;
|
||||
private File fdSocketFile;
|
||||
import tech.xvanturing.freeproxy.data.model.DnsMode;
|
||||
import tech.xvanturing.freeproxy.data.model.ProxyProfile;
|
||||
import tech.xvanturing.freeproxy.data.model.ProxyType;
|
||||
import tech.xvanturing.freeproxy.vpn.TunnelEngine;
|
||||
import tech.xvanturing.freeproxy.vpn.proxy.SocketProtector;
|
||||
|
||||
@Override public void onCreate(){super.onCreate();createNotificationChannel();}
|
||||
@Override public int onStartCommand(Intent intent,int flags,int startId){
|
||||
if(intent==null)return START_NOT_STICKY;String action=intent.getAction();
|
||||
if(ACTION_STOP.equals(action)){appendLog("STOP requested");shutdown("Stopped by user",true);return START_NOT_STICKY;}
|
||||
if(!ACTION_CONNECT.equals(action))return START_NOT_STICKY;
|
||||
cleanupResources(true);clearLog();active=true;running=false;state="Starting VPN";startForeground(NOTIFICATION_ID,buildNotification("Starting full VPN"));
|
||||
String server=intent.getStringExtra("server"),token=intent.getStringExtra("token"),timeout=intent.getStringExtra("timeout"),v4=intent.getStringExtra("vpnIPv4"),v6=intent.getStringExtra("vpnIPv6");
|
||||
int port=intent.getIntExtra("port",53),max=intent.getIntExtra("chunkMax",1048576),min=intent.getIntExtra("chunkMin",32),start=intent.getIntExtra("chunkStart",max);
|
||||
if(server==null||server.trim().isEmpty()){failStart("Server is empty");return START_NOT_STICKY;}if(token==null)token="";if(timeout==null||timeout.isEmpty())timeout="2s";if(v4==null||v6==null){failStart("Missing VPN client address");return START_NOT_STICKY;}
|
||||
start=max;
|
||||
try{
|
||||
establishPacketVpn(v4,v6);
|
||||
state="Starting DragonTCP core";
|
||||
startCore(server.trim(),port,token,start,min,max,timeout.trim(),v4,v6);
|
||||
state="Connecting to DragonTCP server";
|
||||
updateNotification("Connecting • TCP/"+port);
|
||||
}catch(Exception e){failStart(e.getMessage()==null?e.toString():e.getMessage());}
|
||||
public class DragonService extends VpnService {
|
||||
public static final String ACTION_CONNECT = "com.dragontcp.client.CONNECT";
|
||||
public static final String ACTION_STOP = "com.dragontcp.client.STOP";
|
||||
|
||||
public static final String EXTRA_SERVER = "server";
|
||||
public static final String EXTRA_PORT = "port";
|
||||
public static final String EXTRA_TOKEN = "token";
|
||||
public static final String EXTRA_CHUNK_MAX = "chunkMax";
|
||||
public static final String EXTRA_CHUNK_MIN = "chunkMin";
|
||||
public static final String EXTRA_RECONNECT = "reconnect";
|
||||
public static final String EXTRA_TIMEOUT = "timeout";
|
||||
|
||||
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 final Object stateLock = new Object();
|
||||
private volatile Process coreProcess;
|
||||
private volatile TunnelEngine tunnelEngine;
|
||||
private volatile ParcelFileDescriptor tunFd;
|
||||
private volatile boolean connected;
|
||||
private volatile boolean stopping;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
String action = intent != null ? intent.getAction() : null;
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
new Thread(() -> stopEverything("Stopped"), "dragontcp-stop").start();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (ACTION_CONNECT.equals(action)) {
|
||||
startForeground(NOTIFICATION_ID, buildNotification("Starting..."));
|
||||
Intent copy = new Intent(intent);
|
||||
new Thread(() -> startEverything(copy), "dragontcp-start").start();
|
||||
return START_STICKY;
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void establishPacketVpn(String v4,String v6)throws Exception{
|
||||
VpnService.Builder b=new VpnService.Builder();b.setSession("DragonTCP VPN");b.setMtu(1280);
|
||||
b.addAddress(v4,32);b.addAddress(v6,128);b.addRoute("0.0.0.0",0);b.addRoute("::",0);
|
||||
b.addDnsServer("1.1.1.1");b.addDnsServer("2606:4700:4700::1111");
|
||||
try{b.addDisallowedApplication(getPackageName());}catch(PackageManager.NameNotFoundException e){throw new Exception("Cannot exclude DragonTCP from its own VPN",e);}
|
||||
Intent open=new Intent(this,MainActivity.class);open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent pi=PendingIntent.getActivity(this,1,open,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);b.setConfigureIntent(pi);
|
||||
vpnInterface=b.establish();if(vpnInterface==null)throw new Exception("Android did not establish the TUN interface");
|
||||
appendLog("TUN established: "+v4+" + "+v6+" MTU=1280");appendLog("Routes captured: 0.0.0.0/0 and ::/0");appendLog("DNS through VPN: 1.1.1.1 + 2606:4700:4700::1111");appendLog("DragonTCP app UID excluded from VPN to prevent recursion");
|
||||
private void startEverything(Intent intent) {
|
||||
synchronized (stateLock) {
|
||||
if (connected || coreProcess != null || tunnelEngine != null) {
|
||||
// Restart in-place without stopSelf(); this avoids a race where
|
||||
// Android destroys the service just after a new CONNECT begins.
|
||||
stopping = true;
|
||||
cleanupComponentsLocked();
|
||||
}
|
||||
stopping = false;
|
||||
}
|
||||
|
||||
String server = intent.getStringExtra(EXTRA_SERVER);
|
||||
int port = intent.getIntExtra(EXTRA_PORT, 53);
|
||||
String token = intent.getStringExtra(EXTRA_TOKEN);
|
||||
int chunkMax = intent.getIntExtra(EXTRA_CHUNK_MAX, 1024 * 1024);
|
||||
int chunkMin = intent.getIntExtra(EXTRA_CHUNK_MIN, 32);
|
||||
int reconnect = intent.getIntExtra(EXTRA_RECONNECT, 1);
|
||||
int timeout = intent.getIntExtra(EXTRA_TIMEOUT, 2);
|
||||
|
||||
if (server == null || server.trim().isEmpty()) {
|
||||
failStart("Server is required");
|
||||
return;
|
||||
}
|
||||
server = server.trim();
|
||||
if (token == null) token = "";
|
||||
chunkMax = Math.max(32, Math.min(1024 * 1024, chunkMax));
|
||||
chunkMin = Math.max(32, Math.min(chunkMax, chunkMin));
|
||||
reconnect = Math.max(1, reconnect);
|
||||
timeout = Math.max(1, timeout);
|
||||
|
||||
try {
|
||||
AppLog.append("Starting DragonTCP → " + server + ":" + port);
|
||||
Process process = startDragonCore(server, port, token, chunkMax, chunkMin, reconnect, timeout);
|
||||
synchronized (stateLock) { coreProcess = process; }
|
||||
|
||||
startCoreLogReader(process);
|
||||
waitForLocalProxy(process);
|
||||
AppLog.append("Local DragonTCP proxy ready on 127.0.0.1:8080");
|
||||
|
||||
ParcelFileDescriptor pfd = establishVpn();
|
||||
if (pfd == null) throw new IllegalStateException("Android refused to establish the VPN interface");
|
||||
|
||||
SocketProtector protector = new SocketProtector() {
|
||||
@Override public boolean protect(Socket socket) {
|
||||
return DragonService.this.protect(socket);
|
||||
}
|
||||
@Override public boolean protect(DatagramSocket socket) {
|
||||
return DragonService.this.protect(socket);
|
||||
}
|
||||
};
|
||||
|
||||
ProxyProfile profile = new ProxyProfile(ProxyType.HTTP, DnsMode.PROXY, false);
|
||||
InetSocketAddress localProxy = new InetSocketAddress(
|
||||
InetAddress.getByAddress(new byte[]{127, 0, 0, 1}),
|
||||
LOCAL_PROXY_PORT
|
||||
);
|
||||
TunnelEngine engine = new TunnelEngine(
|
||||
pfd,
|
||||
profile,
|
||||
localProxy,
|
||||
TUN_MTU,
|
||||
protector,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
synchronized (stateLock) {
|
||||
tunFd = pfd;
|
||||
tunnelEngine = engine;
|
||||
connected = true;
|
||||
}
|
||||
engine.start();
|
||||
updateNotification("Connected");
|
||||
AppLog.append("VPN connected");
|
||||
AppLog.append("DNS: forced through 1.1.1.1 over DragonTCP");
|
||||
AppLog.append("IPv6: captured and blocked to prevent bypass");
|
||||
} catch (Throwable t) {
|
||||
failStart(t.getMessage() != null ? t.getMessage() : t.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void startCore(String server,int port,String token,int start,int min,int max,String timeout,String v4,String v6)throws Exception{
|
||||
String executable=getApplicationInfo().nativeLibraryDir+"/libdragontcp_vpn.so";File exe=new File(executable);if(!exe.exists())throw new Exception("Embedded DragonTCP VPN core was not extracted");
|
||||
fdSocketFile=new File(getFilesDir(),"dragontcp-tunfd.sock");if(fdSocketFile.exists())fdSocketFile.delete();
|
||||
List<String> cmd=new ArrayList<String>();cmd.add(executable);cmd.add("--server-host");cmd.add(server);cmd.add("--server-port");cmd.add(String.valueOf(port));cmd.add("--token");cmd.add(token);
|
||||
cmd.add("--tun-fd-socket");cmd.add(fdSocketFile.getAbsolutePath());cmd.add("--vpn-ipv4");cmd.add(v4);cmd.add("--vpn-ipv6");cmd.add(v6);cmd.add("--vpn-mtu");cmd.add("1280");
|
||||
cmd.add("--chunk-start");cmd.add(String.valueOf(max));cmd.add("--chunk-max");cmd.add(String.valueOf(max));cmd.add("--chunk-min");cmd.add(String.valueOf(min));cmd.add("--chunk-grow-after");cmd.add("64");cmd.add("--chunk-timeout");cmd.add(timeout);cmd.add("--chunk-reconnect-every");cmd.add("32");cmd.add("--chunk-adapt-log");
|
||||
appendLog("Server: "+server+":"+port);appendLog("Transport chunks: start=max="+max+" min="+min+" pollers=1 timeout="+timeout);
|
||||
ProcessBuilder pb=new ProcessBuilder(cmd);pb.redirectErrorStream(true);pb.directory(getFilesDir());final Process p=pb.start();synchronized(lifecycleLock){process=p;}
|
||||
outputThread=new Thread(()->readCoreOutput(p),"DragonTCP-output");outputThread.setDaemon(true);outputThread.start();
|
||||
passTunFdWhenReady();
|
||||
private Process startDragonCore(
|
||||
String server,
|
||||
int port,
|
||||
String token,
|
||||
int chunkMax,
|
||||
int chunkMin,
|
||||
int reconnect,
|
||||
int timeout
|
||||
) throws Exception {
|
||||
File executable = new File(getApplicationInfo().nativeLibraryDir, "libdragontcp_client.so");
|
||||
if (!executable.exists()) throw new IllegalStateException("Embedded DragonTCP core is missing");
|
||||
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(executable.getAbsolutePath());
|
||||
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));
|
||||
if (!token.isEmpty()) { cmd.add("--token"); cmd.add(token); }
|
||||
cmd.add("--transport"); cmd.add("chunk");
|
||||
cmd.add("--chunk-start"); cmd.add(Integer.toString(chunkMax));
|
||||
cmd.add("--chunk-min"); cmd.add(Integer.toString(chunkMin));
|
||||
cmd.add("--chunk-max"); cmd.add(Integer.toString(chunkMax));
|
||||
cmd.add("--chunk-pollers"); cmd.add("1");
|
||||
cmd.add("--chunk-reconnect-every"); cmd.add(Integer.toString(reconnect));
|
||||
cmd.add("--chunk-timeout"); cmd.add(timeout + "s");
|
||||
cmd.add("--chunk-grow-after"); cmd.add("16");
|
||||
cmd.add("--chunk-adapt-log=true");
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||
pb.redirectErrorStream(true);
|
||||
return pb.start();
|
||||
}
|
||||
|
||||
private void passTunFdWhenReady()throws Exception{
|
||||
long deadline=System.currentTimeMillis()+5000;while(System.currentTimeMillis()<deadline){if(fdSocketFile!=null&&fdSocketFile.exists())break;Process p; synchronized(lifecycleLock){p=process;}if(p==null||!p.isAlive())throw new Exception("DragonTCP core exited before TUN handoff");Thread.sleep(25);}
|
||||
if(fdSocketFile==null||!fdSocketFile.exists())throw new Exception("DragonTCP core did not create its TUN-fd socket");
|
||||
LocalSocket s=new LocalSocket();try{s.connect(new LocalSocketAddress(fdSocketFile.getAbsolutePath(),LocalSocketAddress.Namespace.FILESYSTEM));FileDescriptor fd=vpnInterface.getFileDescriptor();s.setFileDescriptorsForSend(new FileDescriptor[]{fd});s.getOutputStream().write(0x44);s.getOutputStream().flush();appendLog("TUN file descriptor passed to DragonTCP core");}finally{try{s.close();}catch(Exception ignored){}}
|
||||
private void startCoreLogReader(Process process) {
|
||||
Thread reader = new Thread(() -> {
|
||||
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.
|
||||
String lower = line.toLowerCase();
|
||||
if (line.startsWith("adaptive ") || lower.contains("error") || lower.contains("failed")) {
|
||||
AppLog.append(line);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}, "dragontcp-core-log");
|
||||
reader.setDaemon(true);
|
||||
reader.start();
|
||||
|
||||
Thread watcher = new Thread(() -> {
|
||||
try {
|
||||
int code = process.waitFor();
|
||||
boolean shouldStop;
|
||||
synchronized (stateLock) {
|
||||
shouldStop = !stopping && coreProcess == process && connected;
|
||||
}
|
||||
if (shouldStop) {
|
||||
AppLog.append("DragonTCP core exited: " + code);
|
||||
stopEverything("Core stopped");
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}, "dragontcp-core-watch");
|
||||
watcher.setDaemon(true);
|
||||
watcher.start();
|
||||
}
|
||||
|
||||
private void readCoreOutput(Process p){
|
||||
try{BufferedReader br=new BufferedReader(new InputStreamReader(p.getInputStream()));String line;while((line=br.readLine())!=null){appendLog(line);if(line.contains("VPN READY")){running=true;active=true;state="Connected • Full VPN";updateNotification("Connected • IPv4 + IPv6 • TCP/UDP");}}
|
||||
int code=p.waitFor();handleCoreExit(p,code);
|
||||
}catch(Exception e){appendLog("Core reader: "+e);handleCoreExit(p,-1);}
|
||||
private void waitForLocalProxy(Process process) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 10_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);
|
||||
return;
|
||||
} catch (Throwable t) {
|
||||
last = t;
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Local proxy did not start" + (last != null ? ": " + last.getMessage() : ""));
|
||||
}
|
||||
private void handleCoreExit(Process p,int code){boolean owns; synchronized(lifecycleLock){owns=process==p;if(owns)process=null;}if(!owns)return;appendLog("DragonTCP core exited: "+code);running=false;active=false;state="Core exited ("+code+")";closeVpn();stopForeground(true);stopSelf();}
|
||||
|
||||
private Notification buildNotification(String msg){Intent open=new Intent(this,MainActivity.class);open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);PendingIntent op=PendingIntent.getActivity(this,0,open,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);Intent stop=new Intent(this,DragonService.class);stop.setAction(ACTION_STOP);PendingIntent sp=PendingIntent.getService(this,2,stop,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);Notification.Builder nb=Build.VERSION.SDK_INT>=26?new Notification.Builder(this,CHANNEL_ID):new Notification.Builder(this);return nb.setContentTitle("DragonTCP VPN").setContentText(msg).setSmallIcon(android.R.drawable.stat_sys_upload).setOngoing(true).setContentIntent(op).addAction(android.R.drawable.ic_menu_close_clear_cancel,"STOP",sp).build();}
|
||||
private void updateNotification(String m){NotificationManager nm=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);if(nm!=null)nm.notify(NOTIFICATION_ID,buildNotification(m));}
|
||||
private void createNotificationChannel(){if(Build.VERSION.SDK_INT>=26){NotificationChannel c=new NotificationChannel(CHANNEL_ID,"DragonTCP VPN",NotificationManager.IMPORTANCE_LOW);c.setDescription("DragonTCP full packet VPN status");NotificationManager nm=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);if(nm!=null)nm.createNotificationChannel(c);}}
|
||||
private synchronized void appendLog(String line){try(PrintWriter out=new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(getFilesDir(),"dragontcp.log"),true),"UTF-8"))){out.println(line);out.flush();}catch(Exception ignored){}}
|
||||
private void clearLog(){try{new FileOutputStream(new File(getFilesDir(),"dragontcp.log"),false).close();}catch(Exception ignored){}}
|
||||
private void failStart(String m){appendLog("START ERROR: "+m);running=false;active=false;state="Start failed: "+m;cleanupResources(true);stopForeground(true);stopSelf();}
|
||||
private void shutdown(String reason,boolean stop){state="Stopping";running=false;appendLog(reason);cleanupResources(true);active=false;state="Stopped";stopForeground(true);if(stop)stopSelf();}
|
||||
private void cleanupResources(boolean kill){Process p; synchronized(lifecycleLock){p=process;process=null;}if(p!=null){try{p.getInputStream().close();}catch(Exception ignored){}try{p.destroy();}catch(Exception ignored){}if(kill){try{if(!p.waitFor(800,TimeUnit.MILLISECONDS)){p.destroyForcibly();p.waitFor(800,TimeUnit.MILLISECONDS);}}catch(Exception ignored){try{p.destroyForcibly();}catch(Exception ignored2){}}}}Thread t=outputThread;outputThread=null;if(t!=null&&t!=Thread.currentThread())t.interrupt();closeVpn();if(fdSocketFile!=null){fdSocketFile.delete();fdSocketFile=null;}running=false;}
|
||||
private void closeVpn(){ParcelFileDescriptor v=vpnInterface;vpnInterface=null;if(v!=null){try{v.close();}catch(Exception ignored){}}}
|
||||
@Override public void onRevoke(){appendLog("VPN permission revoked");shutdown("VPN revoked",true);super.onRevoke();}
|
||||
@Override public void onDestroy(){cleanupResources(true);active=false;running=false;if(!state.startsWith("Start failed")&&!state.startsWith("Core exited"))state="Stopped";stopForeground(true);super.onDestroy();}
|
||||
private ParcelFileDescriptor establishVpn() throws Exception {
|
||||
Builder builder = new Builder()
|
||||
.setSession("DragonTCP Lite")
|
||||
.setMtu(TUN_MTU)
|
||||
.addAddress("10.77.0.2", 32)
|
||||
.addRoute("0.0.0.0", 0)
|
||||
.addDnsServer("1.1.1.1")
|
||||
// The embedded userspace adapter is intentionally IPv4-only.
|
||||
// Capturing ::/0 blocks IPv6 instead of leaking it outside the VPN.
|
||||
.addAddress("fd77:6472:6167:6f6e::2", 128)
|
||||
.addRoute("::", 0);
|
||||
|
||||
try {
|
||||
builder.addDisallowedApplication(getPackageName());
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
builder.setBlocking(true);
|
||||
builder.setMetered(false);
|
||||
}
|
||||
return builder.establish();
|
||||
}
|
||||
|
||||
private void failStart(String message) {
|
||||
AppLog.append("CONNECT failed: " + message);
|
||||
stopEverything("Failed");
|
||||
}
|
||||
|
||||
private void stopEverything(String logMessage) {
|
||||
synchronized (stateLock) {
|
||||
stopEverythingLocked(logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupComponentsLocked() {
|
||||
connected = false;
|
||||
|
||||
TunnelEngine engine = tunnelEngine;
|
||||
tunnelEngine = null;
|
||||
if (engine != null) {
|
||||
try { engine.stop(); } catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
ParcelFileDescriptor fd = tunFd;
|
||||
tunFd = null;
|
||||
if (fd != null) {
|
||||
try { fd.close(); } catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
Process process = coreProcess;
|
||||
coreProcess = null;
|
||||
if (process != null) {
|
||||
try {
|
||||
process.destroy();
|
||||
if (!process.waitFor(1200, TimeUnit.MILLISECONDS)) {
|
||||
process.destroyForcibly();
|
||||
process.waitFor(800, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
try { process.destroyForcibly(); } catch (Throwable ignored2) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopEverythingLocked(String logMessage) {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
cleanupComponentsLocked();
|
||||
if (logMessage != null) AppLog.append(logMessage);
|
||||
try { stopForeground(true); } catch (Throwable ignored) {}
|
||||
stopSelf();
|
||||
stopping = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoke() {
|
||||
stopEverything("VPN permission revoked");
|
||||
super.onRevoke();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
stopEverything(null);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return super.onBind(intent);
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"DragonTCP VPN",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification(String status) {
|
||||
Intent open = new Intent(this, MainActivity.class);
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(
|
||||
this, 0, open,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)
|
||||
);
|
||||
|
||||
Intent stopIntent = new Intent(this, DragonService.class).setAction(ACTION_STOP);
|
||||
PendingIntent stopPending = PendingIntent.getService(
|
||||
this, 1, stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)
|
||||
);
|
||||
|
||||
Notification.Builder b = Build.VERSION.SDK_INT >= 26
|
||||
? new Notification.Builder(this, CHANNEL_ID)
|
||||
: new Notification.Builder(this);
|
||||
return b.setContentTitle("DragonTCP Lite")
|
||||
.setContentText(status)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_upload_done)
|
||||
.setContentIntent(contentIntent)
|
||||
.setOngoing(true)
|
||||
.addAction(new Notification.Action.Builder(
|
||||
android.R.drawable.ic_menu_close_clear_cancel,
|
||||
"STOP",
|
||||
stopPending
|
||||
).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void updateNotification(String status) {
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
nm.notify(NOTIFICATION_ID, buildNotification(status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package com.dragontcp.client;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.InputType;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
@@ -17,117 +16,254 @@ import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
private static final int VPN_REQUEST = 5301;
|
||||
private static final int VPN_REQUEST = 100;
|
||||
private static final String PREFS = "dragontcp";
|
||||
|
||||
private EditText server, port, token, chunkMax, chunkMin, timeout;
|
||||
private TextView status, logs;
|
||||
private EditText server;
|
||||
private EditText port;
|
||||
private EditText token;
|
||||
private EditText chunkMax;
|
||||
private EditText chunkMin;
|
||||
private EditText reconnect;
|
||||
private EditText timeout;
|
||||
private TextView logText;
|
||||
private ScrollView logScroll;
|
||||
private Button connectButton, stopButton;
|
||||
private Intent pendingServiceIntent;
|
||||
private SharedPreferences prefs;
|
||||
private String lastLogText = "";
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private final Runnable refresher = new Runnable() {
|
||||
@Override public void run() {
|
||||
refreshStatus();
|
||||
handler.postDelayed(this, 500);
|
||||
}
|
||||
};
|
||||
private final AppLog.Listener logListener = line -> runOnUiThread(() -> appendLogLine(line));
|
||||
|
||||
@Override protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
prefs = getSharedPreferences("dragontcp", MODE_PRIVATE);
|
||||
setTitle("DragonTCP VPN");
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
buildUi();
|
||||
loadSettings();
|
||||
handler.post(refresher);
|
||||
}
|
||||
|
||||
private int dp(int v) { return (int)(v * getResources().getDisplayMetrics().density + 0.5f); }
|
||||
private TextView text(String s, float sp, boolean bold) {
|
||||
TextView v = new TextView(this); v.setText(s); v.setTextSize(sp); v.setTextColor(Color.rgb(232,236,241));
|
||||
if (bold) v.setTypeface(Typeface.DEFAULT, Typeface.BOLD); return v;
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
logText.setText(AppLog.history());
|
||||
AppLog.addListener(logListener);
|
||||
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||
}
|
||||
private EditText field(LinearLayout root, String label, int type) {
|
||||
TextView t=text(label,13f,false);t.setPadding(0,dp(9),0,dp(4));root.addView(t);
|
||||
EditText e=new EditText(this);e.setSingleLine(true);e.setTextColor(Color.WHITE);e.setHintTextColor(Color.GRAY);e.setInputType(type);
|
||||
e.setBackgroundColor(Color.rgb(42,47,54));e.setPadding(dp(12),dp(9),dp(12),dp(9));
|
||||
root.addView(e,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));return e;
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
AppLog.removeListener(logListener);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
ScrollView page=new ScrollView(this);page.setFillViewport(true);page.setBackgroundColor(Color.rgb(20,23,27));
|
||||
LinearLayout root=new LinearLayout(this);root.setOrientation(LinearLayout.VERTICAL);root.setPadding(dp(18),dp(18),dp(18),dp(24));page.addView(root);
|
||||
TextView title=text("DragonTCP VPN",27f,true);title.setTextColor(Color.rgb(104,207,255));root.addView(title);
|
||||
TextView sub=text("Full IPv4 / IPv6 packet VPN over adaptive TCP/53",13f,false);sub.setTextColor(Color.rgb(170,179,188));sub.setPadding(0,dp(2),0,dp(12));root.addView(sub);
|
||||
status=text("Stopped",16f,true);status.setPadding(dp(12),dp(12),dp(12),dp(12));status.setBackgroundColor(Color.rgb(34,39,45));root.addView(status);
|
||||
int pad = dp(14);
|
||||
ScrollView outer = new ScrollView(this);
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setPadding(pad, pad, pad, pad);
|
||||
outer.addView(root, new ScrollView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
server=field(root,"Server IP / hostname",InputType.TYPE_CLASS_TEXT);
|
||||
port=field(root,"TCP port",InputType.TYPE_CLASS_NUMBER);
|
||||
token=field(root,"Token",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
chunkMax=field(root,"Maximum transport fragment bytes (start = max)",InputType.TYPE_CLASS_NUMBER);
|
||||
chunkMin=field(root,"Minimum transport fragment bytes",InputType.TYPE_CLASS_NUMBER);
|
||||
timeout=field(root,"Transaction timeout (example: 2s)",InputType.TYPE_CLASS_TEXT);
|
||||
TextView title = new TextView(this);
|
||||
title.setText("DragonTCP Lite VPN");
|
||||
title.setTextSize(24);
|
||||
title.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
|
||||
root.addView(title);
|
||||
|
||||
TextView note=text("Pollers are fixed at 1. Adaptive chunks always start at Max and shrink on failures. All IPv4 and IPv6 routes are captured by the VPN; DragonTCP itself is excluded to prevent a tunnel loop.",12f,false);
|
||||
note.setTextColor(Color.rgb(160,170,180));note.setPadding(0,dp(10),0,dp(8));root.addView(note);
|
||||
TextView subtitle = new TextView(this);
|
||||
subtitle.setText("Android VPN → local HTTP CONNECT proxy → adaptive XOR over TCP/53");
|
||||
subtitle.setTextSize(13);
|
||||
subtitle.setPadding(0, dp(4), 0, dp(12));
|
||||
root.addView(subtitle);
|
||||
|
||||
LinearLayout buttons=new LinearLayout(this);buttons.setOrientation(LinearLayout.HORIZONTAL);buttons.setGravity(Gravity.CENTER);buttons.setPadding(0,dp(8),0,dp(10));root.addView(buttons);
|
||||
connectButton=new Button(this);connectButton.setText("CONNECT");buttons.addView(connectButton,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
stopButton=new Button(this);stopButton.setText("STOP");buttons.addView(stopButton,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
connectButton.setOnClickListener(v -> startDragon()); stopButton.setOnClickListener(v -> stopDragon());
|
||||
TableLayout table = new TableLayout(this);
|
||||
table.setStretchAllColumns(false);
|
||||
table.setColumnStretchable(1, true);
|
||||
root.addView(table, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
LinearLayout lh=new LinearLayout(this);lh.setOrientation(LinearLayout.HORIZONTAL);lh.setGravity(Gravity.CENTER_VERTICAL);root.addView(lh);
|
||||
TextView lt=text("Live log",17f,true);lh.addView(lt,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
Button clear=new Button(this);clear.setText("CLEAR");lh.addView(clear);clear.setOnClickListener(v -> clearLog());
|
||||
logScroll=new ScrollView(this);logScroll.setFillViewport(true);logScroll.setVerticalScrollBarEnabled(true);logScroll.setBackgroundColor(Color.BLACK);
|
||||
logs=text("",11f,false);logs.setTypeface(Typeface.MONOSPACE);logs.setTextIsSelectable(true);logs.setPadding(dp(10),dp(10),dp(10),dp(10));logs.setBackgroundColor(Color.BLACK);
|
||||
logScroll.addView(logs,new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
root.addView(logScroll,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,dp(320)));
|
||||
setContentView(page);
|
||||
server = addField(table, "Server", "", false, false);
|
||||
port = addField(table, "Port", "53", true, false);
|
||||
token = addField(table, "Token", "", false, true);
|
||||
chunkMax = addField(table, "Max chunk", "1048576", true, false);
|
||||
chunkMin = addField(table, "Min chunk", "32", true, false);
|
||||
reconnect = addField(table, "Reconnect every", "1", true, false);
|
||||
timeout = addField(table, "Timeout (s)", "2", true, false);
|
||||
|
||||
TextView fixed = new TextView(this);
|
||||
fixed.setText("Pollers: 1 (fixed) • Start chunk = Max chunk • XOR 0xAD always on");
|
||||
fixed.setTextSize(12);
|
||||
fixed.setPadding(0, dp(8), 0, dp(8));
|
||||
root.addView(fixed);
|
||||
|
||||
LinearLayout buttons = new LinearLayout(this);
|
||||
buttons.setOrientation(LinearLayout.HORIZONTAL);
|
||||
buttons.setGravity(Gravity.CENTER);
|
||||
Button connect = new Button(this);
|
||||
connect.setText("CONNECT");
|
||||
Button stop = new Button(this);
|
||||
stop.setText("STOP");
|
||||
buttons.addView(connect, new LinearLayout.LayoutParams(0, dp(52), 1f));
|
||||
buttons.addView(stop, new LinearLayout.LayoutParams(0, dp(52), 1f));
|
||||
root.addView(buttons);
|
||||
|
||||
connect.setOnClickListener(v -> requestConnect());
|
||||
stop.setOnClickListener(v -> {
|
||||
Intent i = new Intent(this, DragonService.class).setAction(DragonService.ACTION_STOP);
|
||||
startService(i);
|
||||
});
|
||||
|
||||
LinearLayout logHeader = new LinearLayout(this);
|
||||
logHeader.setOrientation(LinearLayout.HORIZONTAL);
|
||||
logHeader.setGravity(Gravity.CENTER_VERTICAL);
|
||||
TextView logLabel = new TextView(this);
|
||||
logLabel.setText("Live log");
|
||||
logLabel.setTextSize(16);
|
||||
logLabel.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
|
||||
Button clear = new Button(this);
|
||||
clear.setText("CLEAR");
|
||||
logHeader.addView(logLabel, new LinearLayout.LayoutParams(0, dp(48), 1f));
|
||||
logHeader.addView(clear, new LinearLayout.LayoutParams(dp(100), dp(48)));
|
||||
root.addView(logHeader);
|
||||
|
||||
logScroll = new ScrollView(this);
|
||||
logText = new TextView(this);
|
||||
logText.setTextSize(12);
|
||||
logText.setTypeface(Typeface.MONOSPACE);
|
||||
logText.setTextIsSelectable(true);
|
||||
logText.setPadding(dp(8), dp(8), dp(8), dp(8));
|
||||
logScroll.addView(logText, new ScrollView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
root.addView(logScroll, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
dp(280)
|
||||
));
|
||||
clear.setOnClickListener(v -> {
|
||||
AppLog.clear();
|
||||
logText.setText("");
|
||||
});
|
||||
|
||||
TextView note = new TextView(this);
|
||||
note.setText("IPv4 is tunneled. IPv6 is captured and blocked so it cannot bypass the proxy. DNS is sent to 1.1.1.1 through DragonTCP using DNS-over-TCP.");
|
||||
note.setTextSize(11);
|
||||
note.setPadding(0, dp(8), 0, dp(12));
|
||||
root.addView(note);
|
||||
|
||||
setContentView(outer);
|
||||
}
|
||||
|
||||
private void loadSettings(){server.setText(prefs.getString("server",""));port.setText(prefs.getString("port","53"));token.setText(prefs.getString("token",""));chunkMax.setText(prefs.getString("chunkMax","1048576"));chunkMin.setText(prefs.getString("chunkMin","32"));timeout.setText(prefs.getString("timeout","2s"));}
|
||||
private int intValue(EditText e,int d){try{return Integer.parseInt(e.getText().toString().trim());}catch(Exception x){return d;}}
|
||||
private boolean validateSettings(){
|
||||
if(server.getText().toString().trim().isEmpty()){toast("Enter the server IP or hostname");return false;}
|
||||
int p=intValue(port,53),min=intValue(chunkMin,32),max=intValue(chunkMax,1048576);
|
||||
if(p<1||p>65535){toast("Port must be 1-65535");return false;}
|
||||
if(min<32||max>1048576||min>max){toast("Chunks must satisfy 32 <= Min <= Max <= 1048576");return false;}
|
||||
if(timeout.getText().toString().trim().isEmpty()){toast("Enter a timeout such as 2s");return false;}
|
||||
return true;
|
||||
private EditText addField(TableLayout table, String label, String defaultValue, boolean numeric, boolean password) {
|
||||
TableRow row = new TableRow(this);
|
||||
row.setPadding(0, dp(2), 0, dp(2));
|
||||
TextView name = new TextView(this);
|
||||
name.setText(label);
|
||||
name.setGravity(Gravity.CENTER_VERTICAL);
|
||||
name.setPadding(0, 0, dp(10), 0);
|
||||
EditText value = new EditText(this);
|
||||
value.setSingleLine(true);
|
||||
value.setText(defaultValue);
|
||||
if (numeric) value.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
if (password) value.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
row.addView(name, new TableRow.LayoutParams(dp(125), dp(50)));
|
||||
row.addView(value, new TableRow.LayoutParams(0, dp(50), 1f));
|
||||
table.addView(row);
|
||||
return value;
|
||||
}
|
||||
private void saveSettings(){prefs.edit().putString("server",server.getText().toString().trim()).putString("port",port.getText().toString().trim()).putString("token",token.getText().toString()).putString("chunkMax",chunkMax.getText().toString().trim()).putString("chunkMin",chunkMin.getText().toString().trim()).putString("timeout",timeout.getText().toString().trim()).apply();}
|
||||
|
||||
private int clientHostId(){
|
||||
int id=prefs.getInt("clientHostId",0);if(id>=2&&id<=65534)return id;
|
||||
id=2+new SecureRandom().nextInt(65533);prefs.edit().putInt("clientHostId",id).apply();return id;
|
||||
private void requestConnect() {
|
||||
try {
|
||||
validateAndSave();
|
||||
} catch (Exception e) {
|
||||
AppLog.append("CONFIG: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
Intent prepare = VpnService.prepare(this);
|
||||
if (prepare != null) {
|
||||
startActivityForResult(prepare, VPN_REQUEST);
|
||||
} else {
|
||||
startDragonService();
|
||||
}
|
||||
}
|
||||
private String clientIPv4(int id){return "10.123."+((id>>8)&255)+"."+(id&255);}
|
||||
private String clientIPv6(int id){return "fd7a:4472:6167:6f6e::"+Integer.toHexString(id);}
|
||||
|
||||
private Intent buildServiceIntent(){
|
||||
int max=intValue(chunkMax,1048576),id=clientHostId();Intent i=new Intent(this,DragonService.class);i.setAction(DragonService.ACTION_CONNECT);
|
||||
i.putExtra("server",server.getText().toString().trim());i.putExtra("port",intValue(port,53));i.putExtra("token",token.getText().toString());
|
||||
i.putExtra("chunkStart",max);i.putExtra("chunkMax",max);i.putExtra("chunkMin",intValue(chunkMin,32));i.putExtra("timeout",timeout.getText().toString().trim());
|
||||
i.putExtra("vpnIPv4",clientIPv4(id));i.putExtra("vpnIPv6",clientIPv6(id));return i;
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == VPN_REQUEST) {
|
||||
if (resultCode == RESULT_OK) startDragonService();
|
||||
else AppLog.append("VPN permission was not granted");
|
||||
}
|
||||
}
|
||||
|
||||
private void startDragonService() {
|
||||
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));
|
||||
i.putExtra(DragonService.EXTRA_TOKEN, p.getString("token", ""));
|
||||
i.putExtra(DragonService.EXTRA_CHUNK_MAX, p.getInt("max", 1048576));
|
||||
i.putExtra(DragonService.EXTRA_CHUNK_MIN, p.getInt("min", 32));
|
||||
i.putExtra(DragonService.EXTRA_RECONNECT, p.getInt("reconnect", 1));
|
||||
i.putExtra(DragonService.EXTRA_TIMEOUT, p.getInt("timeout", 2));
|
||||
if (Build.VERSION.SDK_INT >= 26) startForegroundService(i); else startService(i);
|
||||
}
|
||||
|
||||
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 rec = parse(reconnect, 1, 1000000, "Reconnect every");
|
||||
int tout = parse(timeout, 1, 120, "Timeout");
|
||||
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
.putString("server", h)
|
||||
.putInt("port", p)
|
||||
.putString("token", token.getText().toString())
|
||||
.putInt("max", max)
|
||||
.putInt("min", min)
|
||||
.putInt("reconnect", rec)
|
||||
.putInt("timeout", tout)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private int parse(EditText field, int min, int max, String name) {
|
||||
int v;
|
||||
try { v = Integer.parseInt(field.getText().toString().trim()); }
|
||||
catch (Exception e) { throw new IllegalArgumentException(name + " is invalid"); }
|
||||
if (v < min || v > max) throw new IllegalArgumentException(name + " must be " + min + "-" + max);
|
||||
return v;
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
SharedPreferences p = getSharedPreferences(PREFS, MODE_PRIVATE);
|
||||
server.setText(p.getString("server", ""));
|
||||
port.setText(Integer.toString(p.getInt("port", 53)));
|
||||
token.setText(p.getString("token", ""));
|
||||
chunkMax.setText(Integer.toString(p.getInt("max", 1048576)));
|
||||
chunkMin.setText(Integer.toString(p.getInt("min", 32)));
|
||||
reconnect.setText(Integer.toString(p.getInt("reconnect", 1)));
|
||||
timeout.setText(Integer.toString(p.getInt("timeout", 2)));
|
||||
}
|
||||
|
||||
private void appendLogLine(String line) {
|
||||
// Only auto-scroll when the user was already near the bottom.
|
||||
View child = logScroll.getChildAt(0);
|
||||
int gap = child == null ? 0 : child.getBottom() - (logScroll.getScrollY() + logScroll.getHeight());
|
||||
boolean follow = gap < dp(48);
|
||||
logText.append(line + "\n");
|
||||
if (follow) logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
private void startDragon(){if(!validateSettings())return;saveSettings();pendingServiceIntent=buildServiceIntent();DragonService.active=true;DragonService.state="Waiting for VPN permission";refreshStatus();Intent prep=VpnService.prepare(this);if(prep!=null)startActivityForResult(prep,VPN_REQUEST);else{Intent i=pendingServiceIntent;pendingServiceIntent=null;launchService(i);}}
|
||||
private void launchService(Intent i){if(i==null)return;DragonService.active=true;DragonService.state="Starting full VPN";refreshStatus();if(Build.VERSION.SDK_INT>=26)startForegroundService(i);else startService(i);toast("Starting DragonTCP VPN...");}
|
||||
@Override protected void onActivityResult(int requestCode,int resultCode,Intent data){super.onActivityResult(requestCode,resultCode,data);if(requestCode!=VPN_REQUEST)return;if(resultCode==RESULT_OK&&pendingServiceIntent!=null){Intent i=pendingServiceIntent;pendingServiceIntent=null;launchService(i);}else{pendingServiceIntent=null;DragonService.active=false;DragonService.running=false;DragonService.state="VPN permission denied";refreshStatus();toast("VPN permission is required");}}
|
||||
private void stopDragon(){pendingServiceIntent=null;DragonService.state="Stopping...";refreshStatus();Intent s=new Intent(this,DragonService.class);s.setAction(DragonService.ACTION_STOP);try{startService(s);}catch(Exception e){stopService(new Intent(this,DragonService.class));}handler.postDelayed(()->{if(DragonService.active)stopService(new Intent(MainActivity.this,DragonService.class));refreshStatus();},1800);}
|
||||
private void clearLog(){try{File f=new File(getFilesDir(),"dragontcp.log");new java.io.FileOutputStream(f,false).close();lastLogText="";logs.setText("");}catch(Exception e){toast("Could not clear log: "+e.getMessage());}}
|
||||
private String readTail(File f,int maxBytes){if(!f.exists())return "";try(FileInputStream in=new FileInputStream(f)){long len=f.length();int n=(int)Math.min((long)maxBytes,len);byte[]buf=new byte[n];long skip=len-n;while(skip>0){long s=in.skip(skip);if(s<=0)break;skip-=s;}int off=0;while(off<n){int r=in.read(buf,off,n-off);if(r<0)break;off+=r;}return new String(buf,0,off,"UTF-8");}catch(Exception e){return "log error: "+e.getMessage();}}
|
||||
private void refreshStatus(){boolean a=DragonService.active,r=DragonService.running;status.setText((r?"● ":a?"◐ ":"○ ")+DragonService.state);status.setTextColor(r?Color.rgb(115,235,145):a?Color.rgb(255,205,95):Color.rgb(232,236,241));connectButton.setEnabled(!a);stopButton.setEnabled(a);String current=readTail(new File(getFilesDir(),"dragontcp.log"),131072);if(!current.equals(lastLogText)){lastLogText=current;logs.setText(current);logScroll.post(()->logScroll.fullScroll(View.FOCUS_DOWN));}}
|
||||
private void toast(String s){Toast.makeText(this,s,Toast.LENGTH_LONG).show();}
|
||||
@Override protected void onDestroy(){handler.removeCallbacks(refresher);super.onDestroy();}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user