mirror of https://github.com/ospab/ostp.git
Bump file descriptor limits to fix EMFILE errors
This commit is contained in:
parent
da41289336
commit
922cf0b142
|
|
@ -1461,6 +1461,7 @@ dependencies = [
|
||||||
"pico-args",
|
"pico-args",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rlimit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -1949,6 +1950,15 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rlimit"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "8.11.0"
|
version = "8.11.0"
|
||||||
|
|
|
||||||
|
|
@ -260,30 +260,55 @@ impl ClientConfig {
|
||||||
|
|
||||||
// 3. Outbounds
|
// 3. Outbounds
|
||||||
let mut outbounds = Vec::new();
|
let mut outbounds = Vec::new();
|
||||||
let server_full = json.get("server").and_then(|v| v.as_str()).unwrap_or("127.0.0.1:50000");
|
|
||||||
let server_parts: Vec<&str> = server_full.split(':').collect();
|
|
||||||
let server_host = server_parts.get(0).unwrap_or(&"127.0.0.1");
|
|
||||||
let server_port = server_parts.get(1).unwrap_or(&"50000").parse::<u16>().unwrap_or(50000);
|
|
||||||
let access_key = json.get("access_key").and_then(|v| v.as_str()).unwrap_or("");
|
|
||||||
|
|
||||||
|
let server_full = json.get("server").and_then(|v| v.as_str())
|
||||||
|
.or_else(|| json.get("ostp").and_then(|o| o.get("server_addr")).and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("127.0.0.1:50000");
|
||||||
|
|
||||||
|
let access_key = json.get("access_key").and_then(|v| v.as_str())
|
||||||
|
.or_else(|| json.get("ostp").and_then(|o| o.get("access_key")).and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
let transport_type = json.get("transport").and_then(|t| t.get("mode").or(t.get("type"))).and_then(|v| v.as_str()).unwrap_or("udp");
|
let transport_type = json.get("transport").and_then(|t| t.get("mode").or(t.get("type"))).and_then(|v| v.as_str()).unwrap_or("udp");
|
||||||
let mux_enabled = json.get("mux").and_then(|m| m.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(false);
|
let mux_enabled = json.get("mux").and_then(|m| m.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
let mux_sessions = json.get("mux").and_then(|m| m.get("sessions")).and_then(|v| v.as_u64()).unwrap_or(1);
|
let mux_sessions = json.get("mux").and_then(|m| m.get("sessions")).and_then(|v| v.as_u64()).unwrap_or(1);
|
||||||
|
|
||||||
outbounds.push(serde_json::json!({
|
let servers: Vec<&str> = server_full.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||||
"type": "ostp",
|
let mut ostp_tags = Vec::new();
|
||||||
"tag": "proxy",
|
|
||||||
"server": server_host,
|
for (i, server_str) in servers.iter().enumerate() {
|
||||||
"port": server_port,
|
let server_parts: Vec<&str> = server_str.split(':').collect();
|
||||||
"access_key": access_key,
|
let server_host = server_parts.get(0).unwrap_or(&"127.0.0.1");
|
||||||
"transport": {
|
let server_port = server_parts.get(1).unwrap_or(&"50000").parse::<u16>().unwrap_or(50000);
|
||||||
"type": transport_type
|
|
||||||
},
|
let tag = if servers.len() > 1 { format!("proxy-{}", i) } else { "proxy".to_string() };
|
||||||
"multiplex": {
|
ostp_tags.push(tag.clone());
|
||||||
"enabled": mux_enabled,
|
|
||||||
"sessions": mux_sessions
|
outbounds.push(serde_json::json!({
|
||||||
}
|
"type": "ostp",
|
||||||
}));
|
"tag": tag,
|
||||||
|
"server": server_host,
|
||||||
|
"port": server_port,
|
||||||
|
"access_key": access_key,
|
||||||
|
"transport": {
|
||||||
|
"type": transport_type
|
||||||
|
},
|
||||||
|
"multiplex": {
|
||||||
|
"enabled": mux_enabled,
|
||||||
|
"sessions": mux_sessions
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if servers.len() > 1 {
|
||||||
|
outbounds.push(serde_json::json!({
|
||||||
|
"type": "urltest",
|
||||||
|
"tag": "proxy",
|
||||||
|
"outbounds": ostp_tags,
|
||||||
|
"url": "http://cp.cloudflare.com",
|
||||||
|
"interval": "3m"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
outbounds.push(serde_json::json!({
|
outbounds.push(serde_json::json!({
|
||||||
"type": "direct",
|
"type": "direct",
|
||||||
|
|
|
||||||
|
|
@ -194,14 +194,19 @@ class OstpVpnService : VpnService() {
|
||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
.setSession("OSTP Tunnel")
|
.setSession("OSTP Tunnel")
|
||||||
.addAddress("10.1.0.2", 24)
|
.addAddress("10.1.0.2", 24)
|
||||||
.addAddress("fd00:1:fd00:1:fd00:1:fd00:1", 128)
|
.addAddress("fd00::1", 128)
|
||||||
.addRoute("0.0.0.0", 0)
|
.addRoute("0.0.0.0", 0)
|
||||||
.addRoute("::", 0)
|
.addRoute("::", 0)
|
||||||
.addDnsServer(dnsServer)
|
|
||||||
.setMtu(Math.max(1280, json.optJSONObject("ostp")?.optInt("mtu", 1140) ?: 1140))
|
.setMtu(Math.max(1280, json.optJSONObject("ostp")?.optInt("mtu", 1140) ?: 1140))
|
||||||
|
|
||||||
|
try {
|
||||||
|
builder.addDnsServer(dnsServer)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e("OstpVpnService", "Invalid user DNS server: $dnsServer", e)
|
||||||
|
try { builder.addDnsServer("1.1.1.1") } catch (e2: Throwable) {}
|
||||||
|
}
|
||||||
|
|
||||||
// Always add fallback IPv4 DNS servers
|
// Always add fallback IPv4 DNS servers
|
||||||
try { builder.addDnsServer("1.1.1.1") } catch (e: Throwable) {}
|
|
||||||
try { builder.addDnsServer("8.8.8.8") } catch (e: Throwable) {}
|
try { builder.addDnsServer("8.8.8.8") } catch (e: Throwable) {}
|
||||||
// NOTE: Do NOT add IPv6 DNS servers here — Android would send DNS
|
// NOTE: Do NOT add IPv6 DNS servers here — Android would send DNS
|
||||||
// queries over IPv6, but our smoltcp TUN stack processes them as
|
// queries over IPv6, but our smoltcp TUN stack processes them as
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class OstpProfile {
|
||||||
|
String id;
|
||||||
|
String name;
|
||||||
|
String serverAddr;
|
||||||
|
String accessKey;
|
||||||
|
String transportMode;
|
||||||
|
String stealthSni;
|
||||||
|
bool wss;
|
||||||
|
bool active;
|
||||||
|
|
||||||
|
OstpProfile({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.serverAddr,
|
||||||
|
required this.accessKey,
|
||||||
|
this.transportMode = 'udp',
|
||||||
|
this.stealthSni = '',
|
||||||
|
this.wss = false,
|
||||||
|
this.active = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'serverAddr': serverAddr,
|
||||||
|
'accessKey': accessKey,
|
||||||
|
'transportMode': transportMode,
|
||||||
|
'stealthSni': stealthSni,
|
||||||
|
'wss': wss,
|
||||||
|
'active': active,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory OstpProfile.fromJson(Map<String, dynamic> json) {
|
||||||
|
return OstpProfile(
|
||||||
|
id: json['id'] as String? ?? '',
|
||||||
|
name: json['name'] as String? ?? 'Unnamed Profile',
|
||||||
|
serverAddr: json['serverAddr'] as String? ?? '',
|
||||||
|
accessKey: json['accessKey'] as String? ?? '',
|
||||||
|
transportMode: json['transportMode'] as String? ?? 'udp',
|
||||||
|
stealthSni: json['stealthSni'] as String? ?? '',
|
||||||
|
wss: json['wss'] as bool? ?? false,
|
||||||
|
active: json['active'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import '../models/connection_state_enum.dart';
|
import '../models/connection_state_enum.dart';
|
||||||
|
import '../models/ostp_profile.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
import 'logs_screen.dart';
|
import 'logs_screen.dart';
|
||||||
import 'app_routing_screen.dart';
|
import 'app_routing_screen.dart';
|
||||||
|
|
@ -29,8 +30,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
Timer? _uptimeTimer;
|
Timer? _uptimeTimer;
|
||||||
int _uptimeSecs = 0;
|
int _uptimeSecs = 0;
|
||||||
|
|
||||||
String _serverAddr = '127.0.0.1:443';
|
List<OstpProfile> _activeProfiles = [];
|
||||||
String _accessKey = 'default_key';
|
|
||||||
|
|
||||||
String _download = '0 B';
|
String _download = '0 B';
|
||||||
String _upload = '0 B';
|
String _upload = '0 B';
|
||||||
|
|
@ -71,64 +71,134 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
|
|
||||||
void _loadSettings() {
|
void _loadSettings() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_serverAddr = widget.prefs.getString('server_addr') ?? '127.0.0.1:443';
|
final profilesJson = widget.prefs.getString('profiles_json');
|
||||||
_accessKey = widget.prefs.getString('access_key') ?? '';
|
if (profilesJson != null && profilesJson.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final List<dynamic> decoded = jsonDecode(profilesJson);
|
||||||
|
final profiles = decoded.map((e) => OstpProfile.fromJson(e)).toList();
|
||||||
|
_activeProfiles = profiles.where((p) => p.active).toList();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading profiles: $e');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final oldServer = widget.prefs.getString('server_addr');
|
||||||
|
final oldKey = widget.prefs.getString('access_key');
|
||||||
|
if (oldServer != null && oldServer.isNotEmpty) {
|
||||||
|
final p = OstpProfile(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
name: 'Profile 1',
|
||||||
|
serverAddr: oldServer,
|
||||||
|
accessKey: oldKey ?? '',
|
||||||
|
transportMode: widget.prefs.getString('transport_mode') ?? 'udp',
|
||||||
|
stealthSni: widget.prefs.getString('stealth_sni') ?? '',
|
||||||
|
wss: widget.prefs.getBool('wss') ?? false,
|
||||||
|
active: true
|
||||||
|
);
|
||||||
|
_activeProfiles = [p];
|
||||||
|
widget.prefs.setString('profiles_json', jsonEncode([p.toJson()]));
|
||||||
|
} else {
|
||||||
|
_activeProfiles = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_updateLatestConfigJson();
|
_updateLatestConfigJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateLatestConfigJson() {
|
void _updateLatestConfigJson() {
|
||||||
|
|
||||||
final exDomains = widget.prefs.getString('ex_domains') ?? '';
|
final exDomains = widget.prefs.getString('ex_domains') ?? '';
|
||||||
final exIps = widget.prefs.getString('ex_ips') ?? '';
|
final exIps = widget.prefs.getString('ex_ips') ?? '';
|
||||||
final exProcesses = widget.prefs.getString('ex_processes') ?? '';
|
final exProcesses = widget.prefs.getString('ex_processes') ?? '';
|
||||||
final debugMode = widget.prefs.getBool('debug_mode') ?? false;
|
final debugMode = widget.prefs.getBool('debug_mode') ?? false;
|
||||||
final transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
|
||||||
final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
|
|
||||||
final wss = widget.prefs.getBool('wss') ?? false;
|
|
||||||
final mtu = widget.prefs.getString('mtu') ?? '1140';
|
final mtu = widget.prefs.getString('mtu') ?? '1140';
|
||||||
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
||||||
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2';
|
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2';
|
||||||
|
final tcpFrag = widget.prefs.getBool('tcp_fragmentation') ?? false;
|
||||||
final dnsServer = widget.prefs.getString('dns_server');
|
final dnsServer = widget.prefs.getString('dns_server');
|
||||||
final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer;
|
final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer;
|
||||||
final tunStack = 'ostp';
|
final tunStack = 'ostp';
|
||||||
final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
|
final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
|
||||||
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
|
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
|
||||||
|
|
||||||
final localBind = widget.prefs.getString('local_bind') ?? '127.0.0.1:1088';
|
final localBind = widget.prefs.getString('local_bind') ?? '127.0.0.1:1088';
|
||||||
|
final localParts = localBind.split(':');
|
||||||
|
final localListen = localParts.isNotEmpty ? localParts[0] : '127.0.0.1';
|
||||||
|
final localPort = localParts.length > 1 ? (int.tryParse(localParts[1]) ?? 1088) : 1088;
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> inbounds = [
|
||||||
|
{
|
||||||
|
"type": "local_proxy",
|
||||||
|
"tag": "socks-in",
|
||||||
|
"protocol": "socks",
|
||||||
|
"listen": localListen,
|
||||||
|
"port": localPort
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun-in",
|
||||||
|
"auto_route": true,
|
||||||
|
"mtu": int.tryParse(mtu) ?? 1140
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> outbounds = [];
|
||||||
|
List<String> ostpTags = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < _activeProfiles.length; i++) {
|
||||||
|
final p = _activeProfiles[i];
|
||||||
|
final tag = _activeProfiles.length > 1 ? 'proxy-$i' : 'proxy';
|
||||||
|
ostpTags.add(tag);
|
||||||
|
|
||||||
|
final parts = p.serverAddr.split(':');
|
||||||
|
final host = parts.isNotEmpty ? parts[0] : '127.0.0.1';
|
||||||
|
final port = parts.length > 1 ? (int.tryParse(parts[1]) ?? 50000) : 50000;
|
||||||
|
|
||||||
|
outbounds.add({
|
||||||
|
"type": "ostp",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"port": port,
|
||||||
|
"access_key": p.accessKey,
|
||||||
|
"transport": {
|
||||||
|
"type": p.transportMode,
|
||||||
|
"stealth_sni": p.stealthSni,
|
||||||
|
"wss": p.wss,
|
||||||
|
"tcp_fragmentation": tcpFrag
|
||||||
|
},
|
||||||
|
"multiplex": {
|
||||||
|
"enabled": muxEnabled,
|
||||||
|
"sessions": int.tryParse(muxSessions) ?? 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeProfiles.length > 1) {
|
||||||
|
outbounds.add({
|
||||||
|
"type": "urltest",
|
||||||
|
"tag": "proxy",
|
||||||
|
"outbounds": ostpTags,
|
||||||
|
"url": "http://cp.cloudflare.com",
|
||||||
|
"interval": "3m"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
outbounds.add({"type": "direct", "tag": "direct"});
|
||||||
|
outbounds.add({"type": "block", "tag": "block"});
|
||||||
|
|
||||||
final configMap = {
|
final configMap = {
|
||||||
"mode": "client",
|
"version": "0.3.20",
|
||||||
"debug": debugMode,
|
"log": {
|
||||||
"ostp": {
|
"level": debugMode ? "debug" : "info"
|
||||||
"server_addr": _serverAddr,
|
|
||||||
"local_bind_addr": "0.0.0.0:0",
|
|
||||||
"access_key": _accessKey,
|
|
||||||
"handshake_timeout_ms": 10000,
|
|
||||||
"io_timeout_ms": 5000,
|
|
||||||
"mtu": int.tryParse(mtu) ?? 1140,
|
|
||||||
},
|
},
|
||||||
"local_proxy": {
|
"inbounds": inbounds,
|
||||||
"bind_addr": localBind,
|
"outbounds": outbounds,
|
||||||
"connect_timeout_ms": 15000,
|
"routing": {
|
||||||
},
|
"rules": [
|
||||||
"transport": {
|
{
|
||||||
"mode": transportMode,
|
"domains": exDomains.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
||||||
"stealth_sni": stealthSni,
|
"ips": exIps.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
||||||
"wss": wss,
|
"processes": exProcesses.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
||||||
"tcp_fragmentation": widget.prefs.getBool('tcp_fragmentation') ?? false,
|
"outbound": "direct"
|
||||||
},
|
}
|
||||||
"multiplex": {
|
]
|
||||||
"enabled": muxEnabled,
|
|
||||||
"sessions": int.tryParse(muxSessions) ?? 2,
|
|
||||||
},
|
|
||||||
"tun": {
|
|
||||||
"enable": true,
|
|
||||||
"stack": tunStack
|
|
||||||
},
|
|
||||||
"exclusions": {
|
|
||||||
"domains": exDomains.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
|
||||||
"ips": exIps.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
|
||||||
"processes": exProcesses.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
|
||||||
},
|
},
|
||||||
"app_rules": {
|
"app_rules": {
|
||||||
"mode": appRoutingMode,
|
"mode": appRoutingMode,
|
||||||
|
|
@ -137,6 +207,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
"dns_server": effectiveDnsServer,
|
"dns_server": effectiveDnsServer,
|
||||||
"tun_stack": tunStack
|
"tun_stack": tunStack
|
||||||
};
|
};
|
||||||
|
|
||||||
widget.prefs.setString('latest_config_json', jsonEncode(configMap));
|
widget.prefs.setString('latest_config_json', jsonEncode(configMap));
|
||||||
platform.invokeMethod('saveConfig', {
|
platform.invokeMethod('saveConfig', {
|
||||||
"configJson": jsonEncode(configMap)
|
"configJson": jsonEncode(configMap)
|
||||||
|
|
@ -154,9 +225,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
|
|
||||||
Future<void> _toggleConnection() async {
|
Future<void> _toggleConnection() async {
|
||||||
if (_state == ConnectionStateEnum.disconnected) {
|
if (_state == ConnectionStateEnum.disconnected) {
|
||||||
if (_serverAddr.isEmpty || _accessKey.isEmpty) {
|
if (_activeProfiles.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Please configure Server and Key in Settings')),
|
const SnackBar(content: Text('Please select at least one profile in Settings')),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -176,66 +247,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
final transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
final transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
||||||
final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
|
final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
|
||||||
final wss = widget.prefs.getBool('wss') ?? false;
|
final wss = widget.prefs.getBool('wss') ?? false;
|
||||||
final mtu = widget.prefs.getString('mtu') ?? '1140';
|
_updateLatestConfigJson();
|
||||||
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
final configStr = widget.prefs.getString('latest_config_json') ?? '{}';
|
||||||
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2';
|
|
||||||
final tunStack = 'ostp';
|
|
||||||
|
|
||||||
final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
|
|
||||||
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
|
|
||||||
|
|
||||||
final localBind = widget.prefs.getString('local_bind') ?? '127.0.0.1:1088';
|
|
||||||
final configMap = {
|
|
||||||
"mode": "client",
|
|
||||||
"debug": debugMode,
|
|
||||||
"ostp": {
|
|
||||||
"server_addr": _serverAddr,
|
|
||||||
"local_bind_addr": "0.0.0.0:0",
|
|
||||||
"access_key": _accessKey,
|
|
||||||
"handshake_timeout_ms": 10000,
|
|
||||||
"io_timeout_ms": 5000,
|
|
||||||
"mtu": int.tryParse(mtu) ?? 1140,
|
|
||||||
},
|
|
||||||
"local_proxy": {
|
|
||||||
"bind_addr": localBind,
|
|
||||||
"connect_timeout_ms": 15000,
|
|
||||||
},
|
|
||||||
"transport": {
|
|
||||||
"mode": transportMode,
|
|
||||||
"stealth_sni": stealthSni,
|
|
||||||
"wss": wss,
|
|
||||||
"tcp_fragmentation": widget.prefs.getBool('tcp_fragmentation') ?? false,
|
|
||||||
},
|
|
||||||
"multiplex": {
|
|
||||||
"enabled": muxEnabled,
|
|
||||||
"sessions": int.tryParse(muxSessions) ?? 2,
|
|
||||||
},
|
|
||||||
"tun": {
|
|
||||||
"enable": true,
|
|
||||||
"stack": tunStack
|
|
||||||
},
|
|
||||||
"exclusions": {
|
|
||||||
"domains": exDomains.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
|
||||||
"ips": exIps.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
|
||||||
"processes": exProcesses.split('\n').where((s) => s.trim().isNotEmpty).toList(),
|
|
||||||
},
|
|
||||||
"app_rules": {
|
|
||||||
"mode": appRoutingMode,
|
|
||||||
"packages": appRoutingPackages,
|
|
||||||
},
|
|
||||||
"dns_server": dnsServer,
|
|
||||||
"tun_stack": tunStack
|
|
||||||
};
|
|
||||||
|
|
||||||
widget.prefs.setString('latest_config_json', jsonEncode(configMap));
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('saveConfig', {
|
|
||||||
"configJson": jsonEncode(configMap)
|
|
||||||
});
|
|
||||||
await platform.invokeMethod('startTunnel', {
|
await platform.invokeMethod('startTunnel', {
|
||||||
"configJson": jsonEncode(configMap)
|
"configJson": configStr
|
||||||
});
|
});
|
||||||
|
|
||||||
bool started = false;
|
bool started = false;
|
||||||
|
|
@ -305,9 +322,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
{'t': 'uot', 'w': false, 'r': true},
|
{'t': 'uot', 'w': false, 'r': true},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (_serverAddr.isEmpty || _accessKey.isEmpty) {
|
if (_activeProfiles.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Please configure Server and Key first')),
|
const SnackBar(content: Text('Please select at least one profile first')),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -319,10 +336,14 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
SnackBar(content: Text('Testing: ${mode['t']} | WSS: ${mode['w']} | XTLS: ${mode['r']} | MTU: $mtu'), duration: const Duration(seconds: 2)),
|
SnackBar(content: Text('Testing: ${mode['t']} | WSS: ${mode['w']} | XTLS: ${mode['r']} | MTU: $mtu'), duration: const Duration(seconds: 2)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update prefs
|
// Update prefs and active profile
|
||||||
await widget.prefs.setString('mtu', mtu.toString());
|
await widget.prefs.setString('mtu', mtu.toString());
|
||||||
await widget.prefs.setString('transport_mode', mode['t'] as String);
|
setState(() {
|
||||||
await widget.prefs.setBool('wss', mode['w'] as bool);
|
for (var p in _activeProfiles) {
|
||||||
|
p.transportMode = mode['t'] as String;
|
||||||
|
p.wss = mode['w'] as bool;
|
||||||
|
}
|
||||||
|
});
|
||||||
_updateLatestConfigJson();
|
_updateLatestConfigJson();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -773,7 +794,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||||
const Icon(Icons.dns_rounded, size: 18, color: Colors.white70),
|
const Icon(Icons.dns_rounded, size: 18, color: Colors.white70),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
_serverAddr,
|
_activeProfiles.isNotEmpty ? _activeProfiles.map((e)=>e.name).join(', ') : 'No profile selected',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -156,23 +156,22 @@
|
||||||
<!-- Form card -->
|
<!-- Form card -->
|
||||||
<div class="card scrollable">
|
<div class="card scrollable">
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-server" data-i18n="label_server">Server Address</label>
|
<!-- Profiles Section -->
|
||||||
<input id="in-server" class="field-input" type="text" placeholder="host:port" spellcheck="false" />
|
<div class="profiles-section">
|
||||||
</div>
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 12px;">
|
||||||
|
<span class="field-label">PROFILES</span>
|
||||||
<div class="field-group">
|
<button id="btn-add-profile" class="icon-btn" style="width:24px; height:24px;">+</button>
|
||||||
<label class="field-label" for="in-key" data-i18n="label_key">Access Key</label>
|
</div>
|
||||||
<div class="input-wrap">
|
<div id="profiles-list" class="profiles-list">
|
||||||
<input id="in-key" class="field-input has-icon" type="password" data-i18n-placeholder="ph_key" placeholder="Secure access key" spellcheck="false" />
|
<!-- Profiles will be injected here -->
|
||||||
<button class="peek-btn" id="btn-peek-key" tabindex="-1" aria-label="Show key">
|
</div>
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<div id="profiles-empty" class="profiles-empty" style="display:none; text-align:center; padding: 20px; color: var(--c-text-muted);">
|
||||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
Create a new profile
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<hr style="border:0; border-top: 1px solid var(--c-surface); margin: 24px 0;" />
|
||||||
|
<div class="field-label" style="margin-bottom: 16px;">CLIENT SETTINGS</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<label class="field-label" for="in-socks" data-i18n="label_socks">Local Proxy</label>
|
<label class="field-label" for="in-socks" data-i18n="label_socks">Local Proxy</label>
|
||||||
|
|
@ -184,14 +183,8 @@
|
||||||
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" />
|
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<!-- Transport moved to profile modal -->
|
||||||
<label class="field-label" for="in-transport" data-i18n="label_transport">Transport Protocol</label>
|
|
||||||
<select id="in-transport" class="field-input">
|
|
||||||
<option value="udp" data-i18n="opt_udp">UDP (Default)</option>
|
|
||||||
<option value="uot" data-i18n="opt_uot">TCP (UoT)</option>
|
|
||||||
<option value="dns" data-i18n="opt_dns">DNS Proxy (Last Resort)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="group-dns-proxy" style="display: none; flex-direction: column; gap: 14px;">
|
<div id="group-dns-proxy" style="display: none; flex-direction: column; gap: 14px;">
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
|
|
@ -387,5 +380,39 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
|
|
||||||
|
<!-- Profile Modal -->
|
||||||
|
<div id="profile-modal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 id="profile-modal-title">Edit Profile</h3>
|
||||||
|
<div class="field-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input id="in-prof-name" class="field-input" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label>Server (host:port)</label>
|
||||||
|
<input id="in-prof-server" class="field-input" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label>Access Key</label>
|
||||||
|
<input id="in-prof-key" class="field-input" type="password" />
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label>Transport</label>
|
||||||
|
<select id="in-prof-transport" class="field-input">
|
||||||
|
<option value="udp">UDP</option>
|
||||||
|
<option value="uot">TCP (UoT)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; justify-content:space-between; margin-top: 20px;">
|
||||||
|
<button id="btn-prof-delete" class="danger-btn">Delete</button>
|
||||||
|
<div>
|
||||||
|
<button id="btn-prof-cancel" class="cancel-btn">Cancel</button>
|
||||||
|
<button id="btn-prof-save" class="accent-btn">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ let appState = 'disconnected'; // 'disconnected' | 'connecting' | 'connected'
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
let uptimeTimer = null;
|
let uptimeTimer = null;
|
||||||
let uptimeSecs = 0;
|
let uptimeSecs = 0;
|
||||||
let rawConfig = null; // parsed config.json object
|
let rawConfig = null;
|
||||||
|
let profiles = []; // parsed config.json object
|
||||||
let serverAddr = ''; // current server address (for badge)
|
let serverAddr = ''; // current server address (for badge)
|
||||||
|
|
||||||
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -61,6 +62,20 @@ const toast = $('toast');
|
||||||
|
|
||||||
const btnGoSettings = $('btn-go-settings');
|
const btnGoSettings = $('btn-go-settings');
|
||||||
const btnAutoConnect = $('btn-auto-connect');
|
const btnAutoConnect = $('btn-auto-connect');
|
||||||
|
|
||||||
|
const btnAddProfile = $('btn-add-profile');
|
||||||
|
const profilesList = $('profiles-list');
|
||||||
|
const profilesEmpty = $('profiles-empty');
|
||||||
|
const profileModal = $('profile-modal');
|
||||||
|
const inProfName = $('in-prof-name');
|
||||||
|
const inProfServer = $('in-prof-server');
|
||||||
|
const inProfKey = $('in-prof-key');
|
||||||
|
const inProfTransport = $('in-prof-transport');
|
||||||
|
const btnProfDelete = $('btn-prof-delete');
|
||||||
|
const btnProfCancel = $('btn-prof-cancel');
|
||||||
|
const btnProfSave = $('btn-prof-save');
|
||||||
|
|
||||||
|
let editingProfileId = null;
|
||||||
const btnBack = $('btn-back');
|
const btnBack = $('btn-back');
|
||||||
const btnImport = $('btn-import-url');
|
const btnImport = $('btn-import-url');
|
||||||
const btnPeekKey = $('btn-peek-key');
|
const btnPeekKey = $('btn-peek-key');
|
||||||
|
|
@ -86,8 +101,8 @@ const inLaunchStartup = $('in-launch-startup');
|
||||||
|
|
||||||
function bindSettingsInputs() {
|
function bindSettingsInputs() {
|
||||||
const ids = [
|
const ids = [
|
||||||
'in-server', 'in-key', 'in-socks', 'in-dns',
|
'in-socks', 'in-dns',
|
||||||
'in-transport', 'in-dns-domain', 'in-dns-region',
|
'in-dns-domain', 'in-dns-region',
|
||||||
'in-mtu', 'in-mux-sessions',
|
'in-mtu', 'in-mux-sessions',
|
||||||
'in-tun-mode', 'in-kill-switch', 'in-mux-mode',
|
'in-tun-mode', 'in-kill-switch', 'in-mux-mode',
|
||||||
'in-debug', 'in-autoconnect', 'in-launch-startup'
|
'in-debug', 'in-autoconnect', 'in-launch-startup'
|
||||||
|
|
@ -911,3 +926,90 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function renderProfiles() {
|
||||||
|
if (profiles.length === 0) {
|
||||||
|
profilesList.innerHTML = '';
|
||||||
|
profilesEmpty.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
profilesEmpty.style.display = 'none';
|
||||||
|
profilesList.innerHTML = profiles.map(p => `
|
||||||
|
<div class="profile-item">
|
||||||
|
<input type="checkbox" ${p.active ? 'checked' : ''} onchange="toggleProfile('${p.id}')">
|
||||||
|
<div class="profile-info">
|
||||||
|
<div class="profile-name">${p.name}</div>
|
||||||
|
<div class="profile-addr">${p.serverAddr}</div>
|
||||||
|
</div>
|
||||||
|
<button class="icon-btn" onclick="editProfile('${p.id}')" style="width:24px;height:24px;">✎</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toggleProfile = function(id) {
|
||||||
|
const p = profiles.find(x => x.id === id);
|
||||||
|
if (p) { p.active = !p.active; saveSettings(); renderProfiles(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
window.editProfile = function(id) {
|
||||||
|
editingProfileId = id;
|
||||||
|
const p = profiles.find(x => x.id === id);
|
||||||
|
$('profile-modal-title').innerText = 'Edit Profile';
|
||||||
|
inProfName.value = p.name;
|
||||||
|
inProfServer.value = p.serverAddr;
|
||||||
|
inProfKey.value = p.accessKey;
|
||||||
|
inProfTransport.value = p.transportMode || 'udp';
|
||||||
|
btnProfDelete.style.display = 'block';
|
||||||
|
profileModal.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (btnAddProfile) {
|
||||||
|
btnAddProfile.addEventListener('click', () => {
|
||||||
|
editingProfileId = null;
|
||||||
|
$('profile-modal-title').innerText = 'New Profile';
|
||||||
|
inProfName.value = '';
|
||||||
|
inProfServer.value = '';
|
||||||
|
inProfKey.value = '';
|
||||||
|
inProfTransport.value = 'udp';
|
||||||
|
btnProfDelete.style.display = 'none';
|
||||||
|
profileModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnProfCancel) btnProfCancel.addEventListener('click', () => profileModal.classList.add('hidden'));
|
||||||
|
|
||||||
|
if (btnProfDelete) {
|
||||||
|
btnProfDelete.addEventListener('click', () => {
|
||||||
|
profiles = profiles.filter(x => x.id !== editingProfileId);
|
||||||
|
profileModal.classList.add('hidden');
|
||||||
|
saveSettings();
|
||||||
|
renderProfiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnProfSave) {
|
||||||
|
btnProfSave.addEventListener('click', () => {
|
||||||
|
if (editingProfileId) {
|
||||||
|
const p = profiles.find(x => x.id === editingProfileId);
|
||||||
|
if (p) {
|
||||||
|
p.name = inProfName.value.trim();
|
||||||
|
p.serverAddr = inProfServer.value.trim();
|
||||||
|
p.accessKey = inProfKey.value;
|
||||||
|
p.transportMode = inProfTransport.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profiles.push({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: inProfName.value.trim() || 'New Profile',
|
||||||
|
serverAddr: inProfServer.value.trim(),
|
||||||
|
accessKey: inProfKey.value,
|
||||||
|
transportMode: inProfTransport.value,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
profileModal.classList.add('hidden');
|
||||||
|
saveSettings();
|
||||||
|
renderProfiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1218,3 +1218,14 @@ html[data-theme="light"] .proc-item:hover { background: rgba(0,0,0,0.05); }
|
||||||
border-top: 1px solid var(--c-card-border);
|
border-top: 1px solid var(--c-card-border);
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profiles-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.profile-item { display: flex; align-items: center; background: var(--c-surface); padding: 12px; border-radius: 12px; }
|
||||||
|
.profile-info { flex-grow: 1; margin-left: 12px; }
|
||||||
|
.profile-name { font-weight: bold; font-size: 14px; }
|
||||||
|
.profile-addr { font-size: 11px; color: var(--c-text-muted); }
|
||||||
|
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
|
.modal.hidden { display: none; }
|
||||||
|
.modal-content { background: var(--c-bg); padding: 24px; border-radius: 16px; width: 320px; max-width: 90%; }
|
||||||
|
.danger-btn { background: rgba(255,50,50,0.2); color: #ff5555; padding: 8px 16px; border-radius: 8px; font-weight: bold; }
|
||||||
|
.cancel-btn { background: transparent; color: var(--c-text); padding: 8px 16px; margin-right: 8px; }
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ colored = "2.1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
clipboard-win = "3.1.1"
|
clipboard-win = "3.1.1"
|
||||||
|
rlimit = "0.11.0"
|
||||||
|
|
||||||
[target."cfg(windows)".build-dependencies]
|
[target."cfg(windows)".build-dependencies]
|
||||||
winres = "0.1.12"
|
winres = "0.1.12"
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,7 @@ struct FallbackCfg {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
let _ = rlimit::increase_nofile_limit(1048576);
|
||||||
ostp_client::logging::setup_panic_hook();
|
ostp_client::logging::setup_panic_hook();
|
||||||
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"));
|
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue