diff --git a/app/src/main/assets/singbox/arm64-v8a/config.json b/app/src/main/assets/singbox/arm64-v8a/config.json new file mode 100644 index 0000000..93c1d02 --- /dev/null +++ b/app/src/main/assets/singbox/arm64-v8a/config.json @@ -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" + } + ] + } +} diff --git a/app/src/main/assets/singbox/arm64-v8a/sing-box b/app/src/main/assets/singbox/arm64-v8a/sing-box new file mode 100644 index 0000000..27d1ce3 Binary files /dev/null and b/app/src/main/assets/singbox/arm64-v8a/sing-box differ diff --git a/app/src/main/assets/v2ray/arm64-v8a/config.json b/app/src/main/assets/v2ray/arm64-v8a/config.json index f536d0d..129f3d3 100644 --- a/app/src/main/assets/v2ray/arm64-v8a/config.json +++ b/app/src/main/assets/v2ray/arm64-v8a/config.json @@ -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" -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java index dfa6214..a31059b 100644 --- a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java +++ b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java @@ -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 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 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 getSystemDnsServers() { - List 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 getSystemDnsServers() { + List 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; + } } diff --git a/app/src/main/java/com/example/studyapp/proxy/SingBoxLauncher.java b/app/src/main/java/com/example/studyapp/proxy/SingBoxLauncher.java deleted file mode 100644 index 6ad8eff..0000000 --- a/app/src/main/java/com/example/studyapp/proxy/SingBoxLauncher.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/com/example/studyapp/utils/IpUtil.java b/app/src/main/java/com/example/studyapp/utils/IpUtil.java index 19e699d..6b6b639 100644 --- a/app/src/main/java/com/example/studyapp/utils/IpUtil.java +++ b/app/src/main/java/com/example/studyapp/utils/IpUtil.java @@ -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 { diff --git a/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java b/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java new file mode 100644 index 0000000..965ca3b --- /dev/null +++ b/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java @@ -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 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); + } + } + } + } +} \ No newline at end of file