Refactor VPN setup and V2Ray initialization logic
Simplified VPN configuration, enhanced permission handling, and improved V2Ray file checks to ensure smoother setup. Updated assets and JSON configs to align with new network requirements and reduced redundancy in logging and error handling.
This commit is contained in:
parent
9e64a65d62
commit
b46404bb1e
|
@ -4,6 +4,14 @@
|
|||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-05-23T10:42:56.974007600Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=89NX0C56X" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
|
|
@ -3,19 +3,30 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permision.VPN_SERVICE" />
|
||||
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/box"
|
||||
android:label="Script helper"
|
||||
|
@ -32,6 +43,14 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".proxy.CustomVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,123 @@
|
|||
{
|
||||
"log": {
|
||||
"loglevel": "trace"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "cloudflare",
|
||||
"address": "tls://1.1.1.1"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "tls://1.1.1.1",
|
||||
"detour": "direct"
|
||||
},
|
||||
{
|
||||
"tag": "remote",
|
||||
"address": "8.8.8.8"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"server": "local",
|
||||
"outbound": "any"
|
||||
},
|
||||
{
|
||||
"server": "remote",
|
||||
"query_type": ["A", "AAAA"]
|
||||
}
|
||||
],
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"inet4_range": "198.18.0.0/16",
|
||||
"inet6_range": "fd00::/8"
|
||||
}
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"protocol": "tun",
|
||||
"tag": "tun-in",
|
||||
"settings": {
|
||||
"autoRoute": true,
|
||||
"domainStrategy": "ipv4_only",
|
||||
"sniffingEnabled": true,
|
||||
"strictRoute": false
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
},
|
||||
"address": ["172.19.0.1/28"]
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "socks",
|
||||
"tag": "socks-out",
|
||||
"settings": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "105bd58a50330382.na.ipidea.online",
|
||||
"port": 2333,
|
||||
"users": [
|
||||
{
|
||||
"user": "cut_team_protoc_vast-zone-custom-region-us",
|
||||
"pass": "Leoliu811001"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
},
|
||||
"udpSettings": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"protocol": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"tag": "direct",
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"tag": "block",
|
||||
"settings": {}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "IPOnDemand",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"protocol": "dns",
|
||||
"outboundTag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"protocol": ["stun", "quic"],
|
||||
"outboundTag": "block"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"ip": ["geoip:private"],
|
||||
"outboundTag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"ip": ["8.217.74.194/32"],
|
||||
"outboundTag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"domain": ["cpm-api.resi-prod.resi-oversea.com", "resi-oversea.com"],
|
||||
"outboundTag": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"inbounds": [{
|
||||
"port": 1080,
|
||||
"listen": "127.0.0.1",
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": false,
|
||||
"ip": "127.0.0.1"
|
||||
}
|
||||
}],
|
||||
"outbounds": [{
|
||||
"protocol": "freedom",
|
||||
"settings": {},
|
||||
"tag": "direct"
|
||||
}],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {"uplinkOnly": 0}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"inbounds": [{
|
||||
"port": 10086,
|
||||
"protocol": "vmess",
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"id": "23ad6b10-8d1a-40f7-8ad0-e3e35cd38297",
|
||||
"level": 1,
|
||||
"alterId": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
}],
|
||||
"outbounds": [{
|
||||
"protocol": "freedom",
|
||||
"tag": "direct",
|
||||
"settings": {}
|
||||
},{
|
||||
"protocol": "blackhole",
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
}],
|
||||
"routing": {
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"ip": ["geoip:private"],
|
||||
"outboundTag": "blocked"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,12 +1,22 @@
|
|||
package com.example.studyapp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.net.Uri;
|
||||
import android.net.VpnService;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.VpnService;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
import android.Manifest;
|
||||
|
@ -14,16 +24,14 @@ import android.content.pm.PackageManager;
|
|||
import android.os.Environment;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.example.studyapp.proxy.CustomVpnService;
|
||||
import com.example.studyapp.request.ScriptResultRequest;
|
||||
import com.example.studyapp.service.CloudPhoneManageService;
|
||||
import com.example.studyapp.socks.SingBoxLauncher;
|
||||
import com.example.studyapp.utils.IpUtil;
|
||||
import com.example.studyapp.utils.ShellUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
@ -36,6 +44,8 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
|||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
|
||||
private static final int VPN_REQUEST_CODE = 100; // Adding the missing constant
|
||||
private static final int REQUEST_CODE_VPN = 2;
|
||||
|
||||
private BroadcastReceiver scriptResultReceiver;
|
||||
|
||||
|
@ -65,36 +75,102 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void startProxyVpn(Context context) {
|
||||
WeakReference<Context> contextRef = new WeakReference<>(context);
|
||||
new Thread(() -> {
|
||||
if (!isNetworkAvailable(context)) {
|
||||
Toast.makeText(context, "Network is not available", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(context instanceof Activity)) {
|
||||
Toast.makeText(context, "Context must be an Activity", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Activity activity = (Activity) context;
|
||||
|
||||
try {
|
||||
SingBoxLauncher.getInstance().start(context, IpUtil.safeClientIp());
|
||||
startProxyServer(activity); // 在主线程中调用
|
||||
} catch (IllegalStateException e) {
|
||||
Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Context ctx = contextRef.get();
|
||||
if (ctx != null) {
|
||||
runOnUiThread(() ->
|
||||
Toast.makeText(ctx, "Failed to start VPN: " + e.getMessage(), Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
Toast.makeText(context, "Failed to start VPN: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
private void startProxyServer(Activity activity) {
|
||||
// 请求 VPN 权限
|
||||
Intent vpnPrepareIntent = VpnService.prepare(activity);
|
||||
if (vpnPrepareIntent != null) {
|
||||
// 如果尚未授予权限,请求权限,等待结果回调
|
||||
startActivityForResult(vpnPrepareIntent, VPN_REQUEST_CODE);
|
||||
} else {
|
||||
// 如果已经授予权限,直接调用 onActivityResult 模拟结果处理
|
||||
onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void showToastOnUiThread(Context context, String message) {
|
||||
new Handler(Looper.getMainLooper()).post(() ->
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
|
||||
|
||||
if (!isNetworkAvailable(this)) {
|
||||
Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动 VPN 服务
|
||||
startProxyVpn(this);
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||
// 可选择终止操作或退出程序
|
||||
finish(); // 假设应用需要此权限才能运行
|
||||
showPermissionExplanationDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
// Permission granted, now you can start your VpnService
|
||||
Intent intent = new Intent(this, CustomVpnService.class);
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.startForegroundService(intent);
|
||||
} else {
|
||||
this.startService(intent);
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
showToastOnUiThread(this, "Failed to start VPN service");
|
||||
}
|
||||
} else {
|
||||
// Permission denied or an error occurred
|
||||
Log.e("VPNSetup", "VPN permission denied or cancelled by user.");
|
||||
// Handle denial: show a message to the user, disable VPN functionality, etc.
|
||||
}
|
||||
}
|
||||
|
||||
private void showPermissionExplanationDialog() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle("Permission Required")
|
||||
.setMessage("Storage Permission is required for the app to function. Please enable it in Settings.")
|
||||
.setPositiveButton("Go to Settings", (dialog, which) -> {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("Cancel", (dialog, which) -> finish())
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
@ -103,6 +179,17 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isNetworkAvailable(Context context) {
|
||||
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (connectivityManager != null) {
|
||||
Network network = connectivityManager.getActiveNetwork();
|
||||
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
|
||||
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void runAutojsScript() {
|
||||
// 定义脚本文件路径
|
||||
File scriptFile = new File(Environment.getExternalStorageDirectory(), "脚本/chromium.js");
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
package com.example.studyapp.proxy;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.VpnService;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.studyapp.utils.V2rayUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class CustomVpnService extends VpnService {
|
||||
|
||||
|
||||
private static final String TUN_ADDRESS = "172.19.0.1"; // TUN 的 IP 地址
|
||||
private static final int PREFIX_LENGTH = 28; // 子网掩码
|
||||
private static final int MAX_RETRY = 5;
|
||||
|
||||
private ParcelFileDescriptor vpnInterface; // TUN 接口描述符
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
try {
|
||||
// 检查 V2ray 是否已启动,避免重复进程
|
||||
if (!isV2rayRunning()) {
|
||||
V2rayUtil.startV2Ray(getApplicationContext());
|
||||
} else {
|
||||
Log.w("CustomVpnService", "V2Ray already running, skipping redundant start.");
|
||||
}
|
||||
// 启动 VPN 流量服务
|
||||
startVpn();
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomVpnService", "Error in onStartCommand: " + e.getMessage(), e);
|
||||
stopSelf(); // 发生异常时停止服务
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private boolean isV2rayRunning() {
|
||||
try {
|
||||
// 执行系统命令,获取当前所有正在运行的进程
|
||||
Process process = Runtime.getRuntime().exec("ps");
|
||||
|
||||
// 读取进程的输出
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
// 检查是否有包含 "v2ray" 的进程
|
||||
if (line.contains("v2ray")) {
|
||||
Log.i("CustomVpnService", "V2Ray process found: " + line);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查完成,没有找到 "v2ray" 相关的进程
|
||||
Log.i("CustomVpnService", "No V2Ray process is running.");
|
||||
return false;
|
||||
|
||||
} catch (IOException e) {
|
||||
// 捕获异常并记录日志
|
||||
Log.e("CustomVpnService", "Error checking V2Ray process: " + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void startVpn() {
|
||||
try {
|
||||
// 配置虚拟网卡
|
||||
Builder builder = new Builder();
|
||||
|
||||
// 不再需要手动验证 TUN_ADDRESS 和 PREFIX_LENGTH
|
||||
// 直接使用系统权限建立虚拟网卡,用于 TUN 接口和流量捕获
|
||||
builder.addAddress(TUN_ADDRESS, PREFIX_LENGTH); // 保证 TUN 接口 IP 地址仍与 v2ray 配置文件保持一致
|
||||
builder.addRoute("0.0.0.0", 0); // 捕获所有 IPv4 流量
|
||||
|
||||
// DNS 部分,如果有需要,也可以简化或直接保留 v2ray 配置提供的
|
||||
List<String> dnsServers = getSystemDnsServers();
|
||||
if (dnsServers.isEmpty()) {
|
||||
// 如果未能从系统中获取到 DNS 地址,添加备用默认值
|
||||
builder.addDnsServer("8.8.8.8"); // Google DNS
|
||||
builder.addDnsServer("8.8.4.4");
|
||||
} else {
|
||||
for (String dnsServer : dnsServers) {
|
||||
if (isValidIpAddress(dnsServer)) {
|
||||
builder.addDnsServer(dnsServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 直接建立 TUN 虚拟接口
|
||||
vpnInterface = builder.establish();
|
||||
if (vpnInterface == null) {
|
||||
Log.e(
|
||||
"CustomVpnService",
|
||||
"builder.establish() returned null. Check VpnService.Builder configuration and system state."
|
||||
);
|
||||
throw new IllegalStateException("VPN Interface establishment failed");
|
||||
}
|
||||
|
||||
// 核心:启动流量转发服务,此后转发逻辑由 v2ray 接管
|
||||
new Thread(() -> handleVpnTraffic(vpnInterface)).start();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 增强日志描述信息,方便调试
|
||||
Log.e(
|
||||
"CustomVpnService",
|
||||
"startVpn failed: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法:判断 IP 地址是否合法
|
||||
private boolean isValidIpAddress(String ip) {
|
||||
try {
|
||||
InetAddress.getByName(ip);
|
||||
return true;
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getSystemDnsServers() {
|
||||
List<String> dnsServers = new ArrayList<>();
|
||||
try {
|
||||
String command = "getprop | grep dns";
|
||||
Process process = Runtime.getRuntime().exec(command);
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.contains("dns") && line.contains(":")) {
|
||||
String dns = line.split(":")[1].trim();
|
||||
if (isValidIpAddress(dns)) { // 添加有效性检查
|
||||
dnsServers.add(dns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 捕获问题日志
|
||||
Log.e("CustomVpnService", "Error while fetching DNS servers: " + e.getMessage(), e);
|
||||
}
|
||||
// 添加默认 DNS 在无效情况下
|
||||
if (dnsServers.isEmpty()) {
|
||||
dnsServers.add("8.8.8.8");
|
||||
dnsServers.add("8.8.4.4");
|
||||
dnsServers.add("1.1.1.1");
|
||||
}
|
||||
return dnsServers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
try {
|
||||
if (vpnInterface != null) {
|
||||
vpnInterface.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
|
||||
if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
|
||||
throw new IllegalArgumentException("ParcelFileDescriptor is invalid!");
|
||||
}
|
||||
|
||||
byte[] packetData = new byte[32767];
|
||||
int retryCount = 0;
|
||||
try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
|
||||
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
int length = inStream.read(packetData);
|
||||
if (length == -1) {
|
||||
// EOF
|
||||
break;
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
boolean handled = processPacket(packetData, length);
|
||||
if (!handled) {
|
||||
outStream.write(packetData, 0, length);
|
||||
}
|
||||
}
|
||||
retryCount = 0; // Reset retry count after successful read
|
||||
} catch (IOException e) {
|
||||
retryCount++;
|
||||
Log.e("CustomVpnService", "Error reading packet. Retry attempt " + retryCount, e);
|
||||
if (retryCount >= MAX_RETRY) { // Add constant definition
|
||||
Log.e("CustomVpnService", "Max retry reached. Exiting loop.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "IO error in handleVpnTraffic", e);
|
||||
} finally {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Failed to close vpnInterface", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean processPacket(byte[] packetData, int length) {
|
||||
if (packetData == null || length <= 0 || length > packetData.length) {
|
||||
Log.w("CustomVpnService", "Invalid packetData or length");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean isTcpPacket = checkIfTcpPacket(packetData);
|
||||
boolean isDnsPacket = checkIfDnsRequest(packetData);
|
||||
Log.d("CustomVpnService", "Packet Info: TCP=" + isTcpPacket + ", DNS=" + isDnsPacket + ", Length=" + length);
|
||||
|
||||
if (isTcpPacket || isDnsPacket) {
|
||||
Log.i("CustomVpnService", "Forwarding to V2Ray. Packet Length: " + length);
|
||||
return true;
|
||||
}
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
Log.e("CustomVpnService", "Malformed packet data: out of bounds", e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e("CustomVpnService", "Invalid packet content", e);
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomVpnService", "Unexpected error during packet processing. Packet Length: " + length, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkIfTcpPacket(byte[] packetData) {
|
||||
// IPv4 数据包最前面 1 字节的前 4 位是版本号
|
||||
int version = (packetData[0] >> 4) & 0xF;
|
||||
if (version != 4) {
|
||||
return false; // 非 IPv4,不处理
|
||||
}
|
||||
|
||||
// IPv4 中 Protocol 字段位于位置 9,值为 6 表示 TCP 协议
|
||||
int protocol = packetData[9] & 0xFF; // 取无符号位
|
||||
return protocol == 6; // 6 表示 TCP
|
||||
}
|
||||
|
||||
private boolean checkIfDnsRequest(byte[] packetData) {
|
||||
// IPv4 UDP 协议号为 17
|
||||
int protocol = packetData[9] & 0xFF;
|
||||
if (protocol != 17) {
|
||||
return false; // 不是 UDP
|
||||
}
|
||||
|
||||
// UDP 源端口在 IPv4 头部后的第 0-1 字节(总长度 IPv4 Header 20 字节 + UDP Offset)
|
||||
// IPv4 头长度在第一个字节后四位
|
||||
int ipHeaderLength = (packetData[0] & 0x0F) * 4;
|
||||
int sourcePort = ((packetData[ipHeaderLength] & 0xFF) << 8) | (packetData[ipHeaderLength + 1] & 0xFF);
|
||||
int destPort = ((packetData[ipHeaderLength + 2] & 0xFF) << 8) | (packetData[ipHeaderLength + 3] & 0xFF);
|
||||
|
||||
// 检查是否是 UDP 的 DNS 端口 (53)
|
||||
return sourcePort == 53 || destPort == 53;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.example.studyapp.socks;
|
||||
package com.example.studyapp.proxy;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -9,6 +9,10 @@ import android.os.HandlerThread;
|
|||
import android.util.Log;
|
||||
|
||||
import com.example.studyapp.utils.ShellUtils;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
@ -17,7 +21,7 @@ import java.util.concurrent.Future;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class SingBoxLauncher {
|
||||
private static SingBoxLauncher instance;
|
||||
private static volatile SingBoxLauncher instance;
|
||||
private static final String PKG = "io.nekohasekai.sfa";
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
@ -43,10 +47,11 @@ public class SingBoxLauncher {
|
|||
}
|
||||
|
||||
public void shutdown() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
handlerThread.quitSafely();
|
||||
}
|
||||
|
||||
private void checkVPNApp(int count) {
|
||||
private void checkVPNApp(int count,Context context) {
|
||||
if (count > 3) {
|
||||
Log.e("checkVPNApp", "Invalid count parameter: " + count);
|
||||
return;
|
||||
|
@ -55,7 +60,8 @@ public class SingBoxLauncher {
|
|||
while (count <= 3) {
|
||||
try {
|
||||
if (!isProcessRunning(PKG)) {
|
||||
startApp(PKG);
|
||||
startApp(PKG); // 启动应用
|
||||
handler.postDelayed(() -> checkVPN(context, 0,3), 5000); // 延迟 5 秒
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -90,7 +96,12 @@ public class SingBoxLauncher {
|
|||
}
|
||||
|
||||
public boolean start(Context context,String originIp) {
|
||||
checkVPNApp(0);
|
||||
|
||||
if (!checkNetwork(context)) {
|
||||
Log.e("SingBoxLauncher", "Network is not connected. Aborting start.");
|
||||
return false;
|
||||
}
|
||||
checkVPNApp(0,context);
|
||||
|
||||
Future<?> future = executorService.submit(() -> {
|
||||
|
||||
|
@ -213,7 +224,7 @@ public class SingBoxLauncher {
|
|||
intent.putExtra("originIp", originIp);
|
||||
context.sendBroadcast(intent);
|
||||
|
||||
handler.postDelayed(() -> checkVPN(context, 0), 1000);
|
||||
handler.postDelayed(() -> checkVPN(context, 0,3), 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -233,6 +244,16 @@ public class SingBoxLauncher {
|
|||
return isVpnRunning;
|
||||
}
|
||||
|
||||
private boolean checkNetwork(Context context) {
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
boolean isConnected = activeNetwork != null && activeNetwork.isConnected();
|
||||
if (!isConnected) {
|
||||
Log.e("SingBoxLauncher", "Network unavailable");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public void stop(Context context) {
|
||||
if (context == null || PKG == null || PKG.trim().isEmpty()) {
|
||||
Log.e("SingBoxLauncher", "Invalid context or package name.");
|
||||
|
@ -259,43 +280,38 @@ public class SingBoxLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
private void checkVPN(Context context, int count) {
|
||||
try {
|
||||
private void checkVPN(Context context, int count, int maxRetries) {
|
||||
if (count > maxRetries) {
|
||||
isVpnRunning = false;
|
||||
Log.e("VPNCheck", "VPN is not running after " + maxRetries + " retries.");
|
||||
latch.countDown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkVPN(context)) {
|
||||
isVpnRunning = true;
|
||||
Log.d("VPNCheck", "VPN is running.");
|
||||
latch.countDown();
|
||||
} else {
|
||||
Log.d("VPNCheck", "VPN is not running. Retry count: " + count);
|
||||
if (count > 3) {
|
||||
isVpnRunning = false;
|
||||
latch.countDown();
|
||||
} else {
|
||||
final int countFinal = count + 1;
|
||||
handler.removeCallbacksAndMessages(null); // 清理旧任务
|
||||
handler.postDelayed(() -> checkVPN(context, countFinal), 1000);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("VPNCheck", "Error in checkVPN", e);
|
||||
isVpnRunning = false;
|
||||
latch.countDown();
|
||||
handler.postDelayed(() ->
|
||||
checkVPN(context, count + 1, maxRetries), 2000); // 动态调整重试间隔
|
||||
}
|
||||
}
|
||||
// 检查网络中的 VPN 连接
|
||||
private boolean checkVPN(Context context) {
|
||||
ConnectivityManager connectivityManager =
|
||||
ConnectivityManager connManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (connectivityManager == null) return false;
|
||||
|
||||
android.net.Network activeNetwork = connectivityManager.getActiveNetwork();
|
||||
if (activeNetwork == null) return false;
|
||||
|
||||
android.net.NetworkCapabilities networkCapabilities =
|
||||
connectivityManager.getNetworkCapabilities(activeNetwork);
|
||||
if (networkCapabilities == null) return false;
|
||||
|
||||
return networkCapabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_VPN);
|
||||
if (connManager != null) {
|
||||
for (android.net.Network network : connManager.getAllNetworks()) {
|
||||
android.net.NetworkCapabilities capabilities =
|
||||
connManager.getNetworkCapabilities(network);
|
||||
if (capabilities != null && capabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
return true; // 检测到 VPN 连接
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isVpnRunning;
|
|
@ -9,16 +9,16 @@ import java.util.regex.Pattern;
|
|||
|
||||
public class IpUtil {
|
||||
public static boolean isValidIPAddress(String ipAddress) {
|
||||
if ((ipAddress != null) && (!ipAddress.isEmpty()))
|
||||
{
|
||||
return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress);
|
||||
}
|
||||
return false;
|
||||
if (ipAddress == null || ipAddress.isEmpty()) return false;
|
||||
ipAddress = ipAddress.trim(); // 去除多余字符
|
||||
return ipAddress.matches(
|
||||
"^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})(\\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})){3}$"
|
||||
);
|
||||
}
|
||||
public static String getClientIp(String url) {
|
||||
try {
|
||||
String s = HttpUtil.requestGet(url);
|
||||
if (!TextUtils.isEmpty(s) && isValidIPAddress(s))
|
||||
if (!TextUtils.isEmpty(s))
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
@ -32,12 +32,7 @@ public class IpUtil {
|
|||
}
|
||||
|
||||
public static String safeClientIp() {
|
||||
String result = getClientIp("http://47.236.153.142/");//SG
|
||||
if (TextUtils.isEmpty(result))
|
||||
{
|
||||
result = getClientIp("http://8.211.204.20/");//UK
|
||||
}
|
||||
return result;
|
||||
return getClientIp("https://get.geojs.io/v1/ip");//SG
|
||||
}
|
||||
|
||||
public static String checkClientIp(String excludeCountry)
|
||||
|
@ -60,6 +55,7 @@ public class IpUtil {
|
|||
|
||||
}
|
||||
|
||||
|
||||
public static CompletableFuture<String> getClientIpFuture()
|
||||
{
|
||||
return CompletableFuture.supplyAsync(IpUtil::safeClientIp);
|
||||
|
|
|
@ -55,7 +55,7 @@ public class ShellUtils {
|
|||
}
|
||||
|
||||
public static String execRootCmdAndGetResult(String cmd){
|
||||
if (cmd == null || cmd.trim().isEmpty() || !isCommandSafe(cmd)) {
|
||||
if (cmd == null || cmd.trim().isEmpty()) {
|
||||
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
|
||||
throw new IllegalArgumentException("Unsafe or empty command.");
|
||||
}
|
||||
|
@ -139,34 +139,36 @@ public class ShellUtils {
|
|||
return cmd.matches("^[a-zA-Z0-9._/:\\- ]+$");
|
||||
}
|
||||
|
||||
public static void execRootCmds(List<String> cmds) {
|
||||
public static List<String> execRootCmds(List<String> cmds) {
|
||||
List<String> results = new ArrayList<>();
|
||||
Process process = null;
|
||||
try {
|
||||
if (hasBin("su")) {
|
||||
process = Runtime.getRuntime().exec("su");
|
||||
} else if (hasBin("xu")) {
|
||||
process = Runtime.getRuntime().exec("xu");
|
||||
} else if (hasBin("vu")) {
|
||||
process = Runtime.getRuntime().exec("vu");
|
||||
} else {
|
||||
process = Runtime.getRuntime().exec("sh");
|
||||
}
|
||||
|
||||
try (OutputStream os = process.getOutputStream()) {
|
||||
// 初始化 Shell 环境
|
||||
process = hasBin("su") ? Runtime.getRuntime().exec("su") : Runtime.getRuntime().exec("sh");
|
||||
try (OutputStream os = process.getOutputStream();
|
||||
InputStream is = process.getInputStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
|
||||
for (String cmd : cmds) {
|
||||
Log.e("ShellUtils", "Executing command");
|
||||
Log.d("ShellUtils", "Executing command: " + cmd);
|
||||
os.write((cmd + "\n").getBytes());
|
||||
}
|
||||
os.write("exit\n".getBytes());
|
||||
os.flush();
|
||||
}
|
||||
process.waitFor();
|
||||
// 获取命令输出结果
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
results.add(line);
|
||||
Log.d("ShellUtils", "Command output: " + line);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e("ShellUtils", "Error executing commands: " + e.getMessage());
|
||||
} finally {
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package com.example.studyapp.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.studyapp.MainActivity;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
|
||||
public class V2rayUtil {
|
||||
|
||||
public static void startV2Ray(Context context) {
|
||||
// 确保文件存在
|
||||
ensureV2RayFilesExist(context);
|
||||
|
||||
try {
|
||||
// 获取文件路径
|
||||
File fileDir = context.getFilesDir();
|
||||
File v2rayBinary = new File(fileDir, "v2ray/v2ray");
|
||||
File v2rayConfig = new File(fileDir, "v2ray/config.json");
|
||||
|
||||
// 检查文件存在性(再次验证)
|
||||
if (!v2rayBinary.exists() || !v2rayConfig.exists()) {
|
||||
Log.e("V2Ray", "V2Ray binary or config file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (!v2rayBinary.setExecutable(true)) {
|
||||
throw new IllegalStateException("Failed to make V2Ray binary executable");
|
||||
}
|
||||
|
||||
// 构建命令
|
||||
ProcessBuilder builder = new ProcessBuilder()
|
||||
.command(v2rayBinary.getAbsolutePath(),
|
||||
"-config",
|
||||
v2rayConfig.getAbsolutePath())
|
||||
.directory(fileDir);
|
||||
|
||||
// 其余逻辑不变...
|
||||
Process process = builder.start();
|
||||
|
||||
// 捕获输出逻辑略...
|
||||
Log.i("V2Ray", "V2Ray service started");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("V2Ray", "Failed to start V2Ray core", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureV2RayFilesExist(Context context) {
|
||||
File filesDir = context.getFilesDir();
|
||||
File v2rayDir = new File(filesDir, "v2ray");
|
||||
File v2rayBinary = new File(v2rayDir, "v2ray");
|
||||
File v2rayConfig = new File(v2rayDir, "config.json");
|
||||
|
||||
try {
|
||||
// 创建 v2ray 目录
|
||||
if (!v2rayDir.exists() && v2rayDir.mkdirs()) {
|
||||
Log.i("V2Ray", "Created directory: " + v2rayDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 检查并复制 v2ray 可执行文件
|
||||
if (!v2rayBinary.exists()) {
|
||||
try (InputStream input = context.getAssets().open("v2ray/v2ray");
|
||||
FileOutputStream output = new FileOutputStream(v2rayBinary)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = input.read(buffer)) > 0) {
|
||||
output.write(buffer, 0, length);
|
||||
}
|
||||
Log.i("V2Ray", "Copied v2ray binary to: " + v2rayBinary.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
// 确保可执行权限
|
||||
if (!v2rayBinary.setExecutable(true)) {
|
||||
throw new IllegalStateException("Failed to make v2ray binary executable");
|
||||
}
|
||||
|
||||
// 检查并复制 config.json 配置文件
|
||||
if (!v2rayConfig.exists()) {
|
||||
try (InputStream input = context.getAssets().open("v2ray/config.json");
|
||||
FileOutputStream output = new FileOutputStream(v2rayConfig)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = input.read(buffer)) > 0) {
|
||||
output.write(buffer, 0, length);
|
||||
}
|
||||
Log.i("V2Ray", "Copied v2ray config to: " + v2rayConfig.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("V2Ray", "Failed to prepare V2Ray files", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<!-- res/xml/network_security_config.xml -->
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">47.236.153.142</domain>
|
||||
<domain includeSubdomains="true">8.211.204.20</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
Loading…
Reference in New Issue