**Commit message:**
Replace SingBoxLauncher with SingboxUtil for VPN management Removed the deprecated `SingBoxLauncher` class and introduced `SingboxUtil` for streamlined VPN service management. Simplified the VPN initialization process and improved compatibility with the new Singbox API.
This commit is contained in:
parent
eda9b1013f
commit
be91f9ea3b
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"log": { "level": "trace" },
|
||||
"dns": {
|
||||
"final": "cloudflare",
|
||||
"independent_cache": true,
|
||||
"servers": [
|
||||
{
|
||||
"tag": "cloudflare",
|
||||
"address": "tls://1.1.1.1"
|
||||
},
|
||||
{
|
||||
"tag": "local",
|
||||
"address": "tls://1.1.1.1",
|
||||
"detour": "direct"
|
||||
},
|
||||
{
|
||||
"tag": "remote",
|
||||
"address": "fakeip"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"server": "local",
|
||||
"outbound": "any"
|
||||
},
|
||||
{
|
||||
"server": "remote",
|
||||
"query_type": ["A", "AAAA"]
|
||||
}
|
||||
],
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"inet4_range": "198.18.0.0/15",
|
||||
"inet6_range": "fc00::/18"
|
||||
}
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"tag": "tun-in",
|
||||
"address": ["172.19.0.1/28"],
|
||||
"auto_route": true,
|
||||
"sniff": true,
|
||||
"strict_route": false,
|
||||
"domain_strategy": "ipv4_only"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "socks",
|
||||
"tag": "socks-out",
|
||||
"version": "5",
|
||||
"network": "tcp",
|
||||
"udp_over_tcp": {
|
||||
"enabled": true
|
||||
},
|
||||
"username": "cut_team_protoc_vast-zone-custom-region-us",
|
||||
"password": "Leoliu811001",
|
||||
"server": "105bd58a50330382.na.ipidea.online",
|
||||
"server_port": 2333
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "socks-out",
|
||||
"auto_detect_interface": true,
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"protocol": ["stun", "quic"],
|
||||
"outbound": "block"
|
||||
},
|
||||
{
|
||||
"ip_is_private": true,
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"ip_cidr": "8.217.74.194/32",
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"domain": "cpm-api.resi-prod.resi-oversea.com",
|
||||
"domain_suffix": "resi-oversea.com",
|
||||
"outbound": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -15,13 +15,13 @@
|
|||
},
|
||||
{
|
||||
"tag": "remote",
|
||||
"address": "8.8.8.8"
|
||||
"address": "fakeip"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"server": "local",
|
||||
"outbound": "any"
|
||||
"outboundTag": "any"
|
||||
},
|
||||
{
|
||||
"server": "remote",
|
||||
|
@ -30,8 +30,8 @@
|
|||
],
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"inet4_range": "198.18.0.0/16",
|
||||
"inet6_range": "fd00::/8"
|
||||
"inet4_range": "198.18.0.0/15",
|
||||
"inet6_range": "fc00::/18"
|
||||
}
|
||||
},
|
||||
"inbounds": [
|
||||
|
@ -92,6 +92,7 @@
|
|||
],
|
||||
"routing": {
|
||||
"domainStrategy": "IPOnDemand",
|
||||
"strategy": "rules",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
|
@ -115,10 +116,15 @@
|
|||
},
|
||||
{
|
||||
"type": "field",
|
||||
"domain": ["cpm-api.resi-prod.resi-oversea.com", "resi-oversea.com"],
|
||||
"domain": ["cpm-api.resi-prod.resi-oversea.com"],
|
||||
"outboundTag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"domain": ["resi-oversea.com"],
|
||||
"outboundTag": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tun_address": "172.19.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.studyapp.proxy;
|
||||
|
||||
import static com.example.studyapp.utils.IpUtil.isValidIpAddress;
|
||||
import static com.example.studyapp.utils.V2rayUtil.isV2rayRunning;
|
||||
|
||||
import android.app.Notification;
|
||||
|
@ -16,6 +17,7 @@ import androidx.core.app.NotificationCompat;
|
|||
|
||||
import com.example.studyapp.R;
|
||||
import com.example.studyapp.config.ConfigLoader;
|
||||
import com.example.studyapp.utils.SingboxUtil;
|
||||
import com.example.studyapp.utils.V2rayUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
|
@ -33,321 +35,307 @@ import java.util.List;
|
|||
public class CustomVpnService extends VpnService {
|
||||
|
||||
|
||||
private static final String TUN_ADDRESS = ConfigLoader.getTunnelAddress(); // TUN 的 IP 地址
|
||||
private static final int PREFIX_LENGTH = 28; // 子网掩码
|
||||
private static final String TUN_ADDRESS = ConfigLoader.getTunnelAddress(); // TUN 的 IP 地址
|
||||
private static final int PREFIX_LENGTH = 28; // 子网掩码
|
||||
|
||||
private Thread vpnTrafficThread; // 保存线程引用
|
||||
private ParcelFileDescriptor vpnInterface; // TUN 接口描述符
|
||||
private Thread vpnTrafficThread; // 保存线程引用
|
||||
private ParcelFileDescriptor vpnInterface; // TUN 接口描述符
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
isVpnActive = true; // 服务启动时激活
|
||||
// 开始前台服务
|
||||
startForeground(NOTIFICATION_ID, createNotification());
|
||||
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 static CustomVpnService instance;
|
||||
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
private volatile boolean isVpnActive = false; // 标志位控制数据包处理逻辑
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
isVpnActive = true; // 服务启动时激活
|
||||
// 开始前台服务
|
||||
startForeground(NOTIFICATION_ID, createNotification());
|
||||
try {
|
||||
// 检查 V2ray 是否已启动,避免重复进程
|
||||
|
||||
SingboxUtil.startSingBox(getApplicationContext());
|
||||
// 启动 VPN 流量服务
|
||||
startVpn();
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomVpnService", "Error in onStartCommand: " + e.getMessage(), e);
|
||||
stopSelf(); // 发生异常时停止服务
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
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 流量
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.setBlocking(true);
|
||||
// 直接建立 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
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法:判断 IP 地址是否合法
|
||||
private boolean isValidIpAddress(String ip) {
|
||||
try {
|
||||
InetAddress.getByName(ip);
|
||||
return true;
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
builder.setBlocking(true);
|
||||
// 直接建立 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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 核心:启动流量转发服务,此后转发逻辑由 v2ray 接管
|
||||
new Thread(() -> handleVpnTraffic(vpnInterface)).start();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 增强日志描述信息,方便调试
|
||||
Log.e(
|
||||
"CustomVpnService",
|
||||
"startVpn failed: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法:判断 IP 地址是否合法
|
||||
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
|
||||
// 启动处理流量的线程
|
||||
vpnTrafficThread = new Thread(() -> {
|
||||
if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
|
||||
Log.e("CustomVpnService", "ParcelFileDescriptor is invalid!");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] packetData = new byte[32767]; // 数据包缓冲区
|
||||
|
||||
try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
|
||||
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
|
||||
|
||||
while (!Thread.currentThread().isInterrupted() && isVpnActive) { // 检查线程是否已中断
|
||||
int length;
|
||||
try {
|
||||
length = inStream.read(packetData);
|
||||
if (length == -1) {
|
||||
break; // 读取完成退出
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error reading packet", e);
|
||||
break; // 读取出错退出循环
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
boolean handled = processPacket(packetData, length);
|
||||
if (!handled) {
|
||||
outStream.write(packetData, 0, length); // 未处理的包写回
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error handling VPN traffic", e);
|
||||
} finally {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
// 捕获问题日志
|
||||
Log.e("CustomVpnService", "Error while fetching DNS servers: " + e.getMessage(), e);
|
||||
Log.e("CustomVpnService", "Failed to close vpnInterface", 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;
|
||||
}
|
||||
});
|
||||
vpnTrafficThread.start();
|
||||
}
|
||||
|
||||
private boolean processPacket(byte[] packetData, int length) {
|
||||
if (!isVpnActive) {
|
||||
Log.w("CustomVpnService", "VPN is not active. Skipping packet processing.");
|
||||
return false;
|
||||
}
|
||||
if (packetData == null || length <= 0 || length > packetData.length) {
|
||||
Log.w("CustomVpnService", "Invalid packetData or length");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static CustomVpnService instance;
|
||||
try {
|
||||
boolean isTcpPacket = checkIfTcpPacket(packetData);
|
||||
boolean isDnsPacket = checkIfDnsRequest(packetData);
|
||||
Log.d("CustomVpnService", "Packet Info: TCP=" + isTcpPacket + ", DNS=" + isDnsPacket + ", Length=" + length);
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this; // 在创建时将实例存储到静态字段
|
||||
}
|
||||
|
||||
public static CustomVpnService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private Notification createNotification() {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
"vpn_service",
|
||||
"VPN Service",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
return new NotificationCompat.Builder(this, "vpn_service")
|
||||
.setContentTitle("VPN 服务")
|
||||
.setContentText("VPN 正在运行...")
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
isVpnActive = false; // 服务销毁时停用
|
||||
super.onDestroy();
|
||||
|
||||
// 停止处理数据包的线程
|
||||
if (vpnTrafficThread != null && vpnTrafficThread.isAlive()) {
|
||||
vpnTrafficThread.interrupt(); // 中断线程
|
||||
try {
|
||||
vpnTrafficThread.join(); // 等待线程停止
|
||||
} catch (InterruptedException e) {
|
||||
Log.e("CustomVpnService", "Error while stopping vpnTrafficThread", e);
|
||||
Thread.currentThread().interrupt(); // 重新设置当前线程的中断状态
|
||||
}
|
||||
vpnTrafficThread = null; // 清空线程引用
|
||||
}
|
||||
|
||||
// 关闭 VPN 接口
|
||||
if (vpnInterface != null) {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
|
||||
}
|
||||
vpnInterface = null; // 避免资源泄露
|
||||
}
|
||||
|
||||
// 停止 V2Ray 服务
|
||||
V2rayUtil.stopV2Ray();
|
||||
Log.i("CustomVpnService", "VPN 服务已销毁");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopService(Intent name) {
|
||||
isVpnActive = false; // 服务停止时停用
|
||||
super.stopService(name);
|
||||
|
||||
// 停止处理数据包的线程
|
||||
if (vpnTrafficThread != null && vpnTrafficThread.isAlive()) {
|
||||
vpnTrafficThread.interrupt(); // 中断线程
|
||||
try {
|
||||
vpnTrafficThread.join(); // 等待线程停止
|
||||
} catch (InterruptedException e) {
|
||||
Log.e("CustomVpnService", "Error while stopping vpnTrafficThread", e);
|
||||
Thread.currentThread().interrupt(); // 重新设置当前线程的中断状态
|
||||
}
|
||||
vpnTrafficThread = null; // 清空线程引用
|
||||
}
|
||||
|
||||
// 关闭 VPN 接口
|
||||
if (vpnInterface != null) {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
|
||||
}
|
||||
vpnInterface = null; // 避免资源泄露
|
||||
}
|
||||
|
||||
// 停止 V2Ray 服务
|
||||
V2rayUtil.stopV2Ray();
|
||||
Log.i("CustomVpnService", "VPN 服务已停止");
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopService(Intent name) {
|
||||
isVpnActive = false; // 服务停止时停用
|
||||
super.stopService(name);
|
||||
|
||||
// 停止处理数据包的线程
|
||||
if (vpnTrafficThread != null && vpnTrafficThread.isAlive()) {
|
||||
vpnTrafficThread.interrupt(); // 中断线程
|
||||
try {
|
||||
vpnTrafficThread.join(); // 等待线程停止
|
||||
} catch (InterruptedException e) {
|
||||
Log.e("CustomVpnService", "Error while stopping vpnTrafficThread", e);
|
||||
Thread.currentThread().interrupt(); // 重新设置当前线程的中断状态
|
||||
}
|
||||
vpnTrafficThread = null; // 清空线程引用
|
||||
}
|
||||
|
||||
private volatile boolean isVpnActive = false; // 标志位控制数据包处理逻辑
|
||||
// 关闭 VPN 接口
|
||||
if (vpnInterface != null) {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
|
||||
}
|
||||
vpnInterface = null; // 避免资源泄露
|
||||
}
|
||||
|
||||
// 停止 V2Ray 服务
|
||||
SingboxUtil.stopSingBox();
|
||||
Log.i("CustomVpnService", "VPN 服务已停止");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
|
||||
// 启动处理流量的线程
|
||||
vpnTrafficThread = new Thread(() -> {
|
||||
if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
|
||||
Log.e("CustomVpnService", "ParcelFileDescriptor is invalid!");
|
||||
return;
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
isVpnActive = false; // 服务销毁时停用
|
||||
super.onDestroy();
|
||||
|
||||
// 停止处理数据包的线程
|
||||
if (vpnTrafficThread != null && vpnTrafficThread.isAlive()) {
|
||||
vpnTrafficThread.interrupt(); // 中断线程
|
||||
try {
|
||||
vpnTrafficThread.join(); // 等待线程停止
|
||||
} catch (InterruptedException e) {
|
||||
Log.e("CustomVpnService", "Error while stopping vpnTrafficThread", e);
|
||||
Thread.currentThread().interrupt(); // 重新设置当前线程的中断状态
|
||||
}
|
||||
vpnTrafficThread = null; // 清空线程引用
|
||||
}
|
||||
|
||||
// 关闭 VPN 接口
|
||||
if (vpnInterface != null) {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
|
||||
}
|
||||
vpnInterface = null; // 避免资源泄露
|
||||
}
|
||||
|
||||
// 停止 V2Ray 服务
|
||||
SingboxUtil.stopSingBox();
|
||||
Log.i("CustomVpnService", "VPN 服务已销毁");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this; // 在创建时将实例存储到静态字段
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
byte[] packetData = new byte[32767]; // 数据包缓冲区
|
||||
|
||||
try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
|
||||
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()&&isVpnActive) { // 检查线程是否已中断
|
||||
int length;
|
||||
try {
|
||||
length = inStream.read(packetData);
|
||||
if (length == -1) break; // 读取完成退出
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error reading packet", e);
|
||||
break; // 读取出错退出循环
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
boolean handled = processPacket(packetData, length);
|
||||
if (!handled) {
|
||||
outStream.write(packetData, 0, length); // 未处理的包写回
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Error handling VPN traffic", e);
|
||||
} finally {
|
||||
try {
|
||||
vpnInterface.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("CustomVpnService", "Failed to close vpnInterface", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
vpnTrafficThread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 捕获问题日志
|
||||
Log.e("CustomVpnService", "Error while fetching DNS servers: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
private boolean processPacket(byte[] packetData, int length) {
|
||||
if (!isVpnActive) {
|
||||
Log.w("CustomVpnService", "VPN is not active. Skipping packet processing.");
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
// 添加默认 DNS 在无效情况下
|
||||
if (dnsServers.isEmpty()) {
|
||||
dnsServers.add("8.8.8.8");
|
||||
dnsServers.add("8.8.4.4");
|
||||
dnsServers.add("1.1.1.1");
|
||||
}
|
||||
return dnsServers;
|
||||
}
|
||||
|
||||
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 Notification createNotification() {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
"vpn_service",
|
||||
"VPN Service",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
return new NotificationCompat.Builder(this, "vpn_service")
|
||||
.setContentTitle("VPN 服务")
|
||||
.setContentText("VPN 正在运行...")
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.build();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public static CustomVpnService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,320 +0,0 @@
|
|||
package com.example.studyapp.proxy;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Handler;
|
||||
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;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class SingBoxLauncher {
|
||||
private static volatile SingBoxLauncher instance;
|
||||
private static final String PKG = "io.nekohasekai.sfa";
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
||||
public static SingBoxLauncher getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SingBoxLauncher.class) {
|
||||
if (instance == null) {
|
||||
instance = new SingBoxLauncher();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final HandlerThread handlerThread;
|
||||
|
||||
private SingBoxLauncher() {
|
||||
latch = new CountDownLatch(1);
|
||||
handlerThread = new HandlerThread("SingBoxThread");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
handlerThread.quitSafely();
|
||||
}
|
||||
|
||||
private void checkVPNApp(int count,Context context) {
|
||||
if (count > 3) {
|
||||
Log.e("checkVPNApp", "Invalid count parameter: " + count);
|
||||
return;
|
||||
}
|
||||
|
||||
while (count <= 3) {
|
||||
try {
|
||||
if (!isProcessRunning(PKG)) {
|
||||
startApp(PKG); // 启动应用
|
||||
handler.postDelayed(() -> checkVPN(context, 0,3), 5000); // 延迟 5 秒
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
// 避免 Thread.sleep,改用异步任务定时调度
|
||||
TimeUnit.SECONDS.sleep(2);
|
||||
count++;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // 恢复中断状态
|
||||
Log.e("checkVPNApp", "Thread interrupted: " + e.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
private boolean isProcessRunning(String pkg) {
|
||||
String command = "ps -A | grep " + sanitizePackageName(pkg);
|
||||
String result = ShellUtils.execRootCmdAndGetResult(command);
|
||||
return result != null && !result.isEmpty();
|
||||
}
|
||||
|
||||
private void startApp(String pkg) {
|
||||
ShellUtils.execRootCmd("am start -n " + sanitizePackageName(pkg) + "/.ui.MainActivity");
|
||||
}
|
||||
|
||||
private String sanitizePackageName(String pkg) {
|
||||
if (pkg.matches("^[a-zA-Z0-9._-]+$")) {
|
||||
return pkg;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsafe package name: " + pkg);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean start(Context context,String originIp) {
|
||||
|
||||
if (!checkNetwork(context)) {
|
||||
Log.e("SingBoxLauncher", "Network is not connected. Aborting start.");
|
||||
return false;
|
||||
}
|
||||
checkVPNApp(0,context);
|
||||
|
||||
Future<?> future = executorService.submit(() -> {
|
||||
|
||||
String content = "{\n" +
|
||||
" \"dns\": {\n" +
|
||||
" \"independent_cache\": true,\n" +
|
||||
" \"servers\": [\n" +
|
||||
" {\n" +
|
||||
" \"tag\": \"cloudflare\",\n" +
|
||||
" \"address\": \"tls://1.1.1.1\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"tag\": \"local\",\n" +
|
||||
" \"address\": \"tls://1.1.1.1\",\n" +
|
||||
" \"detour\": \"direct\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"tag\": \"remote\",\n" +
|
||||
" \"address\": \"fakeip\"\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"rules\": [\n" +
|
||||
" {\n" +
|
||||
" \"server\": \"local\",\n" +
|
||||
" \"outbound\": \"any\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"server\": \"remote\",\n" +
|
||||
" \"query_type\": [\n" +
|
||||
" \"A\",\n" +
|
||||
" \"AAAA\"\n" +
|
||||
" ]\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"fakeip\": {\n" +
|
||||
" \"enabled\": true,\n" +
|
||||
" \"inet4_range\": \"198.18.0.0/15\",\n" +
|
||||
" \"inet6_range\": \"fc00::/18\"\n" +
|
||||
" },\n" +
|
||||
" \"final\": \"cloudflare\"\n" +
|
||||
" },\n" +
|
||||
" \"inbounds\": [\n" +
|
||||
" {\n" +
|
||||
" \"type\": \"tun\",\n" +
|
||||
" \"tag\": \"tun-in\",\n" +
|
||||
" \"address\": [\n" +
|
||||
" \"172.19.0.1/28\"\n" +
|
||||
" ],\n" +
|
||||
" \"auto_route\": true,\n" +
|
||||
" \"sniff\": true,\n" +
|
||||
" \"strict_route\": false,\n" +
|
||||
" \"domain_strategy\": \"ipv4_only\"\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"log\": {\n" +
|
||||
" \"level\": \"trace\"\n" +
|
||||
" },\n" +
|
||||
" \"outbounds\": [\n" +
|
||||
" {\n" +
|
||||
" \"type\": \"socks\",\n" +
|
||||
" \"tag\": \"proxy\",\n" +
|
||||
" \"version\": \"5\",\n" +
|
||||
" \"network\": \"tcp\",\n" +
|
||||
" \"udp_over_tcp\": {\n" +
|
||||
" \"enabled\": true\n" +
|
||||
" },\n" +
|
||||
" \"username\": \"cut_team_protoc_vast-zone-custom-region-us\",\n" +
|
||||
" \"password\": \"Leoliu811001\",\n" +
|
||||
" \"server\": \"105bd58a50330382.na.ipidea.online\",\n" +
|
||||
" \"server_port\": 2333\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"type\": \"dns\",\n" +
|
||||
" \"tag\": \"dns-out\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"type\": \"direct\",\n" +
|
||||
" \"tag\": \"direct\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"type\": \"block\",\n" +
|
||||
" \"tag\": \"block\"\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"route\": {\n" +
|
||||
" \"final\": \"proxy\",\n" +
|
||||
" \"auto_detect_interface\": true,\n" +
|
||||
" \"rules\": [\n" +
|
||||
" {\n" +
|
||||
" \"protocol\": \"dns\",\n" +
|
||||
" \"outbound\": \"dns-out\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"protocol\": [\n" +
|
||||
" \"stun\",\n" +
|
||||
" \"quic\"\n" +
|
||||
" ],\n" +
|
||||
" \"outbound\": \"block\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"ip_is_private\": true,\n" +
|
||||
" \"outbound\": \"direct\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"ip_cidr\": \"\",\n" +
|
||||
" \"outbound\": \"direct\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"domain\": \"cpm-api.resi-prod.resi-oversea.com\",\n" +
|
||||
" \"domain_suffix\": \"resi-oversea.com\",\n" +
|
||||
" \"outbound\": \"direct\"\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(PKG + ".action.START_VPN");
|
||||
intent.putExtra("content", content);
|
||||
intent.putExtra("originIp", originIp);
|
||||
context.sendBroadcast(intent);
|
||||
|
||||
handler.postDelayed(() -> checkVPN(context, 0,3), 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
future.get(); // 等待任务完成,捕获异常
|
||||
} catch (Exception e) {
|
||||
Log.e("TaskError", "Task failed to execute properly", e);
|
||||
}
|
||||
|
||||
try {
|
||||
boolean result = latch.await(5, TimeUnit.SECONDS); // 设置超时时间
|
||||
if (!result) {
|
||||
isVpnRunning = false; // 超时回退逻辑
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
isVpnRunning = false;
|
||||
}
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建广播
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(PKG + ".action.STOP_VPN");
|
||||
intent.setPackage(PKG); // 安全性增强,限制接收者
|
||||
context.sendBroadcast(intent);
|
||||
|
||||
// 检查进程是否仍在运行
|
||||
if (isProcessRunning(PKG)) {
|
||||
// 添加延迟等待,确保广播完成
|
||||
Thread.sleep(1000);
|
||||
|
||||
// 执行强制停止命令
|
||||
ShellUtils.execRootCmd("am force-stop " + sanitizePackageName(PKG));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("SingBoxLauncher", "Error while stopping VPN app: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
handler.postDelayed(() ->
|
||||
checkVPN(context, count + 1, maxRetries), 2000); // 动态调整重试间隔
|
||||
}
|
||||
}
|
||||
// 检查网络中的 VPN 连接
|
||||
private boolean checkVPN(Context context) {
|
||||
ConnectivityManager connManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
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;
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
private final Handler handler;
|
||||
}
|
|
@ -2,6 +2,8 @@ package com.example.studyapp.utils;
|
|||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
@ -35,6 +37,15 @@ public class IpUtil {
|
|||
return getClientIp("https://get.geojs.io/v1/ip");//SG
|
||||
}
|
||||
|
||||
public static boolean isValidIpAddress(String ip) {
|
||||
try {
|
||||
InetAddress.getByName(ip);
|
||||
return true;
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static String checkClientIp(String excludeCountry)
|
||||
{
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
package com.example.studyapp.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class SingboxUtil {
|
||||
|
||||
private static File singBoxBinary, singBoxConfig;
|
||||
|
||||
public static void startSingBox(Context context) {
|
||||
if (isSingBoxRunning(context)) {
|
||||
Log.i("SingBox", "singbox is already running. Skipping start.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureSingBoxFilesExist(context)) {
|
||||
Log.e("SingBox", "Singbox files are missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
singBoxBinary.getAbsolutePath(), "run", "-c", singBoxConfig.getAbsolutePath()
|
||||
).redirectErrorStream(true);
|
||||
|
||||
Process process = builder.start();
|
||||
Log.i("SingBox", "SingBox service started.");
|
||||
|
||||
new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
Log.d("SingBox", line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("SingBox", "Error reading process output", e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
int exitCode = process.waitFor(); // 等待进程完成
|
||||
Log.i("SingBox", "SingBox process exited with code: " + exitCode);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e("SingBox", "Failed to start SingBox process", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Log.e("SingBox", "Process was interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并复制 singbox 必需文件
|
||||
public static boolean ensureSingBoxFilesExist(Context context) {
|
||||
synchronized (SingboxUtil.class) {
|
||||
try {
|
||||
// 检查并复制 singbox 可执行文件
|
||||
String abi = Build.SUPPORTED_ABIS[0]; // 获取当前设备支持的 ABI 架构
|
||||
singBoxBinary = new File(context.getCodeCacheDir(), "singbox");
|
||||
|
||||
// 复制二进制文件
|
||||
if (!singBoxBinary.exists()) {
|
||||
try (InputStream binaryInputStream = context.getAssets().open("singbox/" + abi + "/sing-box");
|
||||
FileOutputStream binaryOutputStream = new FileOutputStream(singBoxBinary)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = binaryInputStream.read(buffer)) > 0) {
|
||||
binaryOutputStream.write(buffer, 0, length);
|
||||
}
|
||||
Log.i("SingBox", "Copied singbox binary to: " + singBoxBinary.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
Log.e("SingboxUtil", "Failed to copy singbox binary", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
singBoxBinary.setExecutable(true);
|
||||
singBoxBinary.setReadable(true);
|
||||
singBoxBinary.setWritable(true);
|
||||
|
||||
if (!singBoxBinary.canExecute()) {
|
||||
Log.e("SingboxUtil", "Binary file does not have execute permission. Aborting start.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查并复制配置文件
|
||||
singBoxConfig = new File("/data/singbox/config.json");
|
||||
|
||||
if (!singBoxConfig.exists()) {
|
||||
try (InputStream configInputStream = context.getAssets().open("singbox/" + abi + "/config.json");
|
||||
FileOutputStream configOutputStream = new FileOutputStream(singBoxConfig)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = configInputStream.read(buffer)) > 0) {
|
||||
configOutputStream.write(buffer, 0, length);
|
||||
}
|
||||
Log.i("SingBox", "Copied singbox config.json to: " + singBoxConfig.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
Log.e("SingboxUtil", "Failed to copy config.json", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
singBoxConfig.setReadable(true);
|
||||
singBoxConfig.setWritable(true);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.e("SingboxUtil", "Error in ensureSingBoxFilesExist method", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否有正在运行的 singbox
|
||||
public static boolean isSingBoxRunning(Context context) {
|
||||
android.app.ActivityManager activityManager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
java.util.List<android.app.ActivityManager.RunningAppProcessInfo> runningProcesses = activityManager.getRunningAppProcesses();
|
||||
if (runningProcesses != null) {
|
||||
for (android.app.ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
|
||||
if (processInfo.processName.contains("sing-box")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void stopSingBox() {
|
||||
Process process = null;
|
||||
try {
|
||||
// 检查是否支持 `pkill` 并停止 sing-box 进程
|
||||
ProcessBuilder builder = new ProcessBuilder("pkill", "-f", "sing-box");
|
||||
process = builder.start();
|
||||
|
||||
// 等待进程执行完成
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode == 0) {
|
||||
Log.i("SingBox", "singbox process stopped successfully.");
|
||||
} else {
|
||||
Log.e("SingBox", "Failed to stop singbox process. Exit code: " + exitCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("SingBox", "IOException occurred while trying to stop singbox", e);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e("SingBox", "The stopSingBox process was interrupted", e);
|
||||
Thread.currentThread().interrupt(); // 恢复中断状态
|
||||
} catch (Exception e) {
|
||||
Log.e("SingBox", "Unexpected error occurred", e);
|
||||
} finally {
|
||||
// 确保关闭流以避免资源泄露
|
||||
if (process != null) {
|
||||
try {
|
||||
process.getInputStream().close();
|
||||
process.getErrorStream().close();
|
||||
} catch (IOException e) {
|
||||
Log.e("SingBox", "Failed to close process streams", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue