#include "db/ConfigBuilder.hpp" #include "db/Database.hpp" #include "fmt/includes.h" #include "fmt/Preset.hpp" #include #include #include namespace NekoRay { // Common QSharedPointer BuildConfig(const QSharedPointer &ent, bool forTest) { if (IS_NEKO_BOX) { return BuildConfigSingBox(ent, forTest); } return BuildConfigV2Ray(ent, forTest); } QString BuildChain(int chainId, const QSharedPointer &status) { // Make list QList> ents; auto ent = status->ent; auto result = status->result; if (ent->type == "chain") { auto list = ent->ChainBean()->list; std::reverse(std::begin(list), std::end(list)); for (auto id: list) { ents += profileManager->GetProfile(id); if (ents.last() == nullptr) { result->error = QString("chain missing ent: %1").arg(id); return {}; } if (ents.last()->type == "chain") { result->error = QString("chain in chain is not allowed: %1").arg(id); return {}; } } } else { ents += ent; } // BuildChain QString chainTagOut = BuildChainInternal(0, ents, status); // Chain ent traffic stat if (ents.length() > 1) { status->ent->traffic_data->id = status->ent->id; status->ent->traffic_data->tag = chainTagOut.toStdString(); status->result->outboundStats += status->ent->traffic_data; } return chainTagOut; } // V2Ray void ApplyCustomOutboundJsonSettings(const QJsonObject &custom, QJsonObject &outbound) { // 合并 if (custom.isEmpty()) return; for (const auto &key: custom.keys()) { if (outbound.contains(key)) { auto v = custom[key]; auto v_orig = outbound[key]; if (v.isObject() && v_orig.isObject()) {// isObject 则合并? auto vo = v.toObject(); QJsonObject vo_orig = v_orig.toObject(); ApplyCustomOutboundJsonSettings(vo, vo_orig); outbound[key] = vo_orig; } else { outbound[key] = v; } } else { outbound[key] = custom[key]; } } } QSharedPointer BuildConfigV2Ray(const QSharedPointer &ent, bool forTest) { auto result = QSharedPointer(new BuildConfigResult); auto status = QSharedPointer(new BuildConfigStatus); status->ent = ent; status->result = result; // Log auto logObj = QJsonObject{{"loglevel", dataStore->log_level}}; result->coreConfig.insert("log", logObj); // Inbounds QJsonObject sniffing{{"destOverride", dataStore->fake_dns ? QJsonArray{"fakedns", "http", "tls", "quic"} : QJsonArray{"http", "tls", "quic"}}, {"enabled", true}, {"metadataOnly", false}, {"routeOnly", dataStore->sniffing_mode == SniffingMode::FOR_ROUTING},}; // socks-in if (InRange(dataStore->inbound_socks_port, 0, 65535) && !forTest) { QJsonObject socksInbound; socksInbound["tag"] = "socks-in"; socksInbound["protocol"] = "socks"; socksInbound["listen"] = dataStore->inbound_address; socksInbound["port"] = dataStore->inbound_socks_port; socksInbound["settings"] = QJsonObject({{"auth", "noauth"}, {"udp", true},}); if (dataStore->fake_dns || dataStore->sniffing_mode != SniffingMode::DISABLE) { socksInbound["sniffing"] = sniffing; } status->inbounds += socksInbound; } // http-in if (InRange(dataStore->inbound_http_port, 0, 65535) && !forTest) { QJsonObject socksInbound; socksInbound["tag"] = "http-in"; socksInbound["protocol"] = "http"; socksInbound["listen"] = dataStore->inbound_address; socksInbound["port"] = dataStore->inbound_http_port; if (dataStore->sniffing_mode != SniffingMode::DISABLE) { socksInbound["sniffing"] = sniffing; } status->inbounds += socksInbound; } // Outbounds auto tagProxy = BuildChain(0, status); if (!result->error.isEmpty()) return result; // direct & bypass & block status->outbounds += QJsonObject{{"protocol", "freedom"}, {"tag", "direct"},}; status->outbounds += QJsonObject{{"protocol", "freedom"}, {"tag", "bypass"},}; status->outbounds += QJsonObject{{"protocol", "blackhole"}, {"tag", "block"},}; // block for tun if (!forTest) { status->routingRules += QJsonObject{{"type", "field"}, {"ip", QJsonArray{"224.0.0.0/3", "169.254.0.0/16",},}, {"outboundTag", "block"},}; status->routingRules += QJsonObject{{"type", "field"}, {"port", "135-139"}, {"outboundTag", "block"},}; } // DNS Routing (tun2socks 用到,防污染) if (dataStore->dns_routing && !forTest) { QJsonObject dnsOut; dnsOut["protocol"] = "dns"; dnsOut["tag"] = "dns-out"; QJsonObject dnsOut_settings; dnsOut_settings["network"] = "tcp"; dnsOut_settings["port"] = 53; dnsOut_settings["address"] = "8.8.8.8"; dnsOut_settings["userLevel"] = 1; dnsOut["settings"] = dnsOut_settings; dnsOut["proxySettings"] = QJsonObject{{"tag", tagProxy}, {"transportLayer", true}}; status->outbounds += dnsOut; status->routingRules += QJsonObject{{"type", "field"}, {"port", "53"}, {"inboundTag", QJsonArray{"socks-in", "http-in"}}, {"outboundTag", "dns-out"},}; status->routingRules += QJsonObject{{"type", "field"}, {"inboundTag", QJsonArray{"dns-in"}}, {"outboundTag", "dns-out"},}; } // custom inbound QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray()) result->coreConfig.insert("inbounds", status->inbounds); result->coreConfig.insert("outbounds", status->outbounds); // dns domain user rule for (const auto &line: SplitLines(dataStore->routing->proxy_domain)) { if (line.startsWith("#")) continue; if (dataStore->dns_routing) status->domainListDNSRemote += line; status->domainListRemote += line; } for (const auto &line: SplitLines(dataStore->routing->direct_domain)) { if (line.startsWith("#")) continue; if (dataStore->dns_routing) status->domainListDNSDirect += line; status->domainListDirect += line; } for (const auto &line: SplitLines(dataStore->routing->block_domain)) { if (line.startsWith("#")) continue; status->domainListBlock += line; } // final add DNS QJsonObject dns; QJsonArray dnsServers; // Remote or FakeDNS QJsonObject dnsServerRemote; dnsServerRemote["address"] = dataStore->fake_dns ? "fakedns" : dataStore->remote_dns; dnsServerRemote["domains"] = status->domainListDNSRemote; if (!forTest) dnsServers += dnsServerRemote; // Direct auto directDnsAddress = dataStore->direct_dns; if (directDnsAddress.contains("://")) { auto directDnsIp = SubStrBefore(SubStrAfter(directDnsAddress, "://"), "/"); if (IsIpAddress(directDnsIp)) { status->routingRules.push_front(QJsonObject{{"type", "field"}, {"ip", QJsonArray{directDnsIp}}, {"outboundTag", "direct"},}); } else { status->routingRules.push_front(QJsonObject{{"type", "field"}, {"domain", QJsonArray{directDnsIp}}, {"outboundTag", "direct"},}); } } else if (directDnsAddress != "localhost") { status->routingRules.push_front(QJsonObject{{"type", "field"}, {"ip", QJsonArray{directDnsAddress}}, {"outboundTag", "direct"},}); } dnsServers += QJsonObject{{"address", directDnsAddress}, {"domains", status->domainListDNSDirect}, {"skipFallback", true},}; dns["disableFallbackIfMatch"] = true; dns["servers"] = dnsServers; dns["tag"] = "dns"; result->coreConfig.insert("dns", dns); // Routing QJsonObject routing; routing["domainStrategy"] = dataStore->domain_strategy; routing["domainMatcher"] = dataStore->domain_matcher == DomainMatcher::MPH ? "mph" : "linear"; // ip user rule QJsonObject routingRule_tmp; routingRule_tmp["type"] = "field"; // block routingRule_tmp["outboundTag"] = "block"; for (const auto &line: SplitLines(dataStore->routing->block_ip)) { if (line.startsWith("#")) continue; status->ipListBlock += line; } // final add block route if (!status->ipListBlock.isEmpty()) { auto tmp = routingRule_tmp; tmp["ip"] = status->ipListBlock; status->routingRules += tmp; } if (!status->domainListBlock.isEmpty()) { auto tmp = routingRule_tmp; tmp["domain"] = status->domainListBlock; status->routingRules += tmp; } // proxy routingRule_tmp["outboundTag"] = tagProxy; for (const auto &line: SplitLines(dataStore->routing->proxy_ip)) { if (line.startsWith("#")) continue; status->ipListRemote += line; } // final add proxy route if (!status->ipListRemote.isEmpty()) { auto tmp = routingRule_tmp; tmp["ip"] = status->ipListRemote; status->routingRules += tmp; } if (!status->domainListRemote.isEmpty()) { auto tmp = routingRule_tmp; tmp["domain"] = status->domainListRemote; status->routingRules += tmp; } // bypass routingRule_tmp["outboundTag"] = "bypass"; for (const auto &line: SplitLines(dataStore->routing->direct_ip)) { if (line.startsWith("#")) continue; status->ipListDirect += line; } // final add bypass route if (!status->ipListDirect.isEmpty()) { auto tmp = routingRule_tmp; tmp["ip"] = status->ipListDirect; status->routingRules += tmp; } if (!status->domainListDirect.isEmpty()) { auto tmp = routingRule_tmp; tmp["domain"] = status->domainListDirect; status->routingRules += tmp; } // final add routing rule // custom routing rule auto routingRules = QString2QJsonObject(dataStore->routing->custom)["rules"].toArray(); QJSONARRAY_ADD(routingRules, QString2QJsonObject(dataStore->custom_route_global)["rules"].toArray()) QJSONARRAY_ADD(routingRules, status->routingRules) routing["rules"] = routingRules; result->coreConfig.insert("routing", routing); // Policy & stats QJsonObject policy; QJsonObject levels; QJsonObject level1; level1["connIdle"] = 30; levels["1"] = level1; policy["levels"] = levels; QJsonObject policySystem; policySystem["statsOutboundDownlink"] = true; policySystem["statsOutboundUplink"] = true; policy["system"] = policySystem; result->coreConfig.insert("policy", policy); result->coreConfig.insert("stats", QJsonObject()); return result; } QString BuildChainInternal(int chainId, const QList> &ents, const QSharedPointer &status) { QString chainTag = "c-" + Int2String(chainId); bool muxApplied = false; QString pastTag; int index = 0; for (const auto &ent: ents) { // tagOut: v2ray outbound tag for a profile // profile2 (in) (global) tag g-(id) // profile1 tag (chainTag)-(id) // profile0 (out) tag (chainTag)-(id) / single: chainTag=g-(id) auto tagOut = chainTag + "-" + Int2String(ent->id); // needGlobal: can only contain one? bool needGlobal = false; // first profile set as global if (index == ents.length() - 1) { needGlobal = true; tagOut = "g-" + Int2String(ent->id); if (chainId == 0) { tagOut = "proxy"; } } if (needGlobal) { if (status->globalProfiles.contains(ent->id)) { continue; } status->globalProfiles += ent->id; } if (index > 0) { // chain rules: past if (!ents[index - 1]->bean->NeedExternal()) { auto replaced = status->outbounds.last().toObject(); if (IS_NEKO_BOX) { replaced["detour"] = tagOut; } else { replaced["proxySettings"] = QJsonObject{{"tag", tagOut}, {"transportLayer", true},}; } status->outbounds.removeLast(); status->outbounds += replaced; } else { if (IS_NEKO_BOX) { status->routingRules += QJsonObject{{"inbound", QJsonArray{pastTag + "-mapping"}}, {"outbound", tagOut},}; } else { status->routingRules += QJsonObject{{"type", "field"}, {"inboundTag", QJsonArray{pastTag + "-mapping"}}, {"outboundTag", tagOut},}; } } } else { // index == 0 means last profile in chain / not chain chainTag = tagOut; status->result->outboundStat = ent->traffic_data; } // chain rules: this auto mapping_port = MkPort(); if (ent->bean->NeedExternal()) { if (IS_NEKO_BOX) { status->inbounds += QJsonObject{{"type", "direct"}, {"tag", tagOut + "-mapping"}, {"listen", "127.0.0.1"}, {"listen_port", mapping_port}, {"override_address", ent->bean->serverAddress}, {"override_port", ent->bean->serverPort},}; } else { status->inbounds += QJsonObject{{"protocol", "dokodemo-door"}, {"tag", tagOut + "-mapping"}, {"listen", "127.0.0.1"}, {"port", mapping_port}, {"settings", QJsonObject{ // to {"address", ent->bean->serverAddress}, {"port", ent->bean->serverPort}, {"network", "tcp,udp"},}},}; } // no chain rule and not outbound, so need to set to direct if (index == ents.length() - 1) { if (IS_NEKO_BOX) { status->routingRules += QJsonObject{{"inbound", QJsonArray{tagOut + "-mapping"}}, {"outbound", "direct"},}; } else { status->routingRules += QJsonObject{{"type", "field"}, {"inboundTag", QJsonArray{tagOut + "-mapping"}}, {"outboundTag", "direct"},}; } } } // Outbound QJsonObject outbound; fmt::CoreObjOutboundBuildResult coreR; fmt::ExternalBuildResult extR; if (ent->bean->NeedExternal()) { auto ext_socks_port = MkPort(); extR = ent->bean->BuildExternal(mapping_port, ext_socks_port); if (!extR.error.isEmpty()) { // rejected status->result->error = extR.error; return ""; } // SOCKS OUTBOUND if (IS_NEKO_BOX) { outbound["type"] = "socks"; outbound["server"] = "127.0.0.1"; outbound["server_port"] = ext_socks_port; } else { outbound["protocol"] = "socks"; QJsonObject settings; QJsonArray servers; QJsonObject server; server["address"] = "127.0.0.1"; server["port"] = ext_socks_port; servers.push_back(server); settings["servers"] = servers; outbound["settings"] = settings; } // EXTERNAL PROCESS auto extC = new sys::ExternalProcess(); extC->tag = ent->bean->DisplayType(); extC->program = extR.program; extC->arguments = extR.arguments; extC->env = extR.env; status->result->ext += extC; } else { coreR = IS_NEKO_BOX ? ent->bean->BuildCoreObjSingBox() : ent->bean->BuildCoreObjV2Ray(); if (coreR.outbound.isEmpty()) { status->result->error = "unsupported outbound"; return {}; } if (!coreR.error.isEmpty()) { // rejected status->result->error = coreR.error; return {}; } outbound = coreR.outbound; } // outbound misc outbound["tag"] = tagOut; ent->traffic_data->id = ent->id; ent->traffic_data->tag = tagOut.toStdString(); status->result->outboundStats += ent->traffic_data; if (IS_NEKO_BOX) { // TODO no such field? auto ds = dataStore->outbound_domain_strategy; if (ds == "UseIPv4") { ds = "ipv4_only"; } else if (ds == "UseIPv6") { ds = "ipv6_only"; } else if (ds == "PreferIPv4") { ds = "prefer_ipv4"; } else if (ds == "PreferIPv6") { ds = "prefer_ipv6"; } else { ds = ""; } outbound["domain_strategy"] = ds; // TODO apply mux } else { outbound["domainStrategy"] = dataStore->outbound_domain_strategy; // apply mux if (dataStore->mux_cool > 0 && !muxApplied) { // TODO refactor mux settings if (ent->type == "vmess" || ent->type == "trojan" || ent->type == "vless") { auto muxObj = QJsonObject{{"enabled", true}, {"concurrency", dataStore->mux_cool},}; auto stream = GetStreamSettings(ent->bean.data()); if (stream != nullptr && !stream->packet_encoding.isEmpty()) { muxObj["packetEncoding"] = stream->packet_encoding; } outbound["mux"] = muxObj; muxApplied = true; } } } // apply custom outbound settings auto custom_item = ent->bean->_get("custom"); if (custom_item != nullptr) { ApplyCustomOutboundJsonSettings(QString2QJsonObject(*((QString *) custom_item->ptr)), outbound); } // Bypass Lookup for the first profile if (index == ents.length() - 1 && !IsIpAddress(ent->bean->serverAddress)) { if (dataStore->enhance_resolve_server_domain && !IS_NEKO_BOX) { status->result->tryDomains += ent->bean->serverAddress; } else { status->domainListDNSDirect += "full:" + ent->bean->serverAddress; } } status->outbounds += outbound; pastTag = tagOut; index++; } return chainTag; } // SingBox QSharedPointer BuildConfigSingBox(const QSharedPointer &ent, bool forTest) { auto result = QSharedPointer(new BuildConfigResult); auto status = QSharedPointer(new BuildConfigStatus); status->ent = ent; status->result = result; // Log result->coreConfig["log"] = QJsonObject{ {"level", dataStore->log_level.replace("warning", "warn").replace("none", "panic")}}; // Inbounds // mixed-in if (InRange(dataStore->inbound_socks_port, 0, 65535) && !forTest) { QJsonObject socksInbound; socksInbound["tag"] = "mixed-in"; socksInbound["type"] = "mixed"; socksInbound["listen"] = dataStore->inbound_address; socksInbound["listen_port"] = dataStore->inbound_socks_port; if (dataStore->sniffing_mode != SniffingMode::DISABLE) { socksInbound["sniff"] = true; socksInbound["sniff_override_destination"] = dataStore->sniffing_mode == SniffingMode::FOR_DESTINATION; } status->inbounds += socksInbound; } // Outbounds auto tagProxy = BuildChain(0, status); if (!result->error.isEmpty()) return result; // direct & bypass & block status->outbounds += QJsonObject{{"type", "direct"}, {"tag", "direct"},}; status->outbounds += QJsonObject{{"type", "direct"}, {"tag", "bypass"},}; status->outbounds += QJsonObject{{"type", "block"}, {"tag", "block"},}; status->outbounds += QJsonObject{{"type", "dns"}, {"tag", "dns-out"},}; // custom inbound QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray()) result->coreConfig.insert("inbounds", status->inbounds); result->coreConfig.insert("outbounds", status->outbounds); // dns domain user rule for (const auto &line: SplitLines(dataStore->routing->proxy_domain)) { if (line.startsWith("#")) continue; status->domainListDNSRemote += line; status->domainListRemote += line; } for (const auto &line: SplitLines(dataStore->routing->direct_domain)) { if (line.startsWith("#")) continue; status->domainListDNSDirect += line; status->domainListDirect += line; } for (const auto &line: SplitLines(dataStore->routing->block_domain)) { if (line.startsWith("#")) continue; status->domainListBlock += line; } // auto make_rule = [&](const QJsonArray &arr, bool isIP = false) { QJsonObject rule; QJsonArray ips; QJsonArray geoips; QJsonArray domain_keyword; QJsonArray domain_subdomain; QJsonArray domain_full; QJsonArray domain_suffix; QJsonArray geosites; for (const auto &domain_: arr) { auto domain = domain_.toString(); if (isIP) { if (domain.startsWith("geoip:")) { geoips += domain.replace("geoip:", ""); } else { ips += domain; } } else { if (domain.startsWith("geosite:")) { geosites += domain.replace("geosite:", ""); } else if (domain.startsWith("full:")) { domain_full += domain.replace("full:", ""); } else if (domain.startsWith("domain:")) { domain_suffix += domain.replace("domain:", ""); } else { domain_keyword += domain; } } } if (isIP) { if (ips.isEmpty() && geoips.isEmpty()) return rule; rule["ip_cidr"] = ips; rule["geoip"] = geoips; } else { if (domain_keyword.isEmpty() && domain_subdomain.isEmpty() && domain_full.isEmpty() && geosites.isEmpty()) { return rule; } rule["domain"] = domain_full; rule["domain_suffix"] = domain_suffix; rule["domain_keyword"] = domain_keyword; rule["geosite"] = geosites; } return rule; }; // final add DNS QJsonObject dns; QJsonArray dnsServers; QJsonArray dnsRules; // Remote if (!forTest) dnsServers += QJsonObject{{"tag", "dns-remote"}, {"address_resolver", "dns-local"}, {"address", dataStore->remote_dns}, {"detour", tagProxy},}; // Direct auto directDNSAddress = dataStore->direct_dns; if (directDNSAddress == "localhost") directDNSAddress = "local"; if (!forTest) dnsServers += QJsonObject{{"tag", "dns-direct"}, {"address_resolver", "dns-local"}, {"address", directDNSAddress.replace("+local://", "://")}, {"detour", "bypass"},}; // local dnsServers += QJsonObject{{"tag", "dns-local"}, {"address", "local"},}; // DNS rules auto add_rule_dns = [&](const QJsonArray &arr, const QString &server) { auto rule = make_rule(arr, false); if (rule.isEmpty()) return; rule["server"] = server; dnsRules += rule; }; add_rule_dns(status->domainListDNSRemote, "dns-remote"); add_rule_dns(status->domainListDNSDirect, "dns-direct"); dns["servers"] = dnsServers; dns["rules"] = dnsRules; result->coreConfig.insert("dns", dns); // Routing // custom routing rule auto routingRules = QString2QJsonObject(dataStore->routing->custom)["rules"].toArray(); auto add_rule_route = [&](const QJsonArray &arr, bool isIP, const QString &out) { auto rule = make_rule(arr, isIP); if (rule.isEmpty()) return; rule["outbound"] = out; routingRules += rule; }; // ip user rule add_rule_route(status->ipListBlock, true, "block"); add_rule_route(status->ipListRemote, true, tagProxy); add_rule_route(status->ipListDirect, true, "bypass"); // domain user rule add_rule_route(status->domainListBlock, false, "block"); add_rule_route(status->domainListRemote, false, tagProxy); add_rule_route(status->domainListDirect, false, "bypass"); // dns hijack routingRules += QJsonObject{{"protocol", "dns"}, {"outbound", "dns-out"}}; // geopath auto geoip = FindCoreAsset("geoip.db"); auto geosite = FindCoreAsset("geosite.db"); if (geoip.isEmpty()) result->error = + "geoip.db not found"; if (geosite.isEmpty()) result->error = + "geosite.db not found"; // final add routing rule QJSONARRAY_ADD(routingRules, QString2QJsonObject(dataStore->custom_route_global)["rules"].toArray()) QJSONARRAY_ADD(routingRules, status->routingRules) result->coreConfig.insert("route", QJsonObject{ {"rules", routingRules}, {"auto_detect_interface", true}, {"geoip", QJsonObject{{"path", geoip},},}, {"geosite", QJsonObject{{"path", geosite},},} }); // api result->coreConfig.insert("experimental", QJsonObject{ {"v2ray_api", QJsonObject{ {"listen", "127.0.0.1:" + Int2String(dataStore->inbound_socks_port + 10)}, {"stats", QJsonObject{ {"enabled", true}, {"outbounds", QJsonArray{ tagProxy, "bypass", "block" }}, }} }}, }); return result; } QString WriteVPNSingBoxConfig() { // QString process_name_rule = dataStore->vpn_bypass_process.trimmed(); if (!process_name_rule.isEmpty()) { auto arr = SplitLines(process_name_rule); QJsonObject rule{{"outbound", "direct"}, {"process_name", QList2QJsonArray(arr)}}; process_name_rule = "," + QJsonObject2QString(rule, false); } QString cidr_rule = dataStore->vpn_bypass_cidr.trimmed(); if (!cidr_rule.isEmpty()) { auto arr = SplitLines(cidr_rule); QJsonObject rule{{"outbound", "direct"}, {"ip_cidr", QList2QJsonArray(arr)}}; cidr_rule = "," + QJsonObject2QString(rule, false); } // gen config auto configFn = ":/neko/vpn/sing-box-vpn.json"; if (QFile::exists("vpn/sing-box-vpn.json")) configFn = "vpn/sing-box-vpn.json"; auto config = ReadFileText(configFn) .replace("%IPV6_ADDRESS%", dataStore->vpn_ipv6 ? R"("inet6_address": "fdfe:dcba:9876::1/126",)" : "") .replace("%MTU%", Int2String(dataStore->vpn_mtu)) .replace("%STACK%", Preset::SingBox::VpnImplementation.value(dataStore->vpn_implementation)) .replace("%PROCESS_NAME_RULE%", process_name_rule) .replace("%CIDR_RULE%", cidr_rule) .replace("%PORT%", Int2String(dataStore->inbound_socks_port)); // write config QFile file; file.setFileName(QFileInfo(configFn).fileName()); file.open(QIODevice::ReadWrite | QIODevice::Truncate); file.write(config.toUtf8()); file.close(); return QFileInfo(file).absoluteFilePath(); } QString WriteVPNLinuxScript(const QString &protectPath, const QString &configPath) { // gen script auto scriptFn = ":/neko/vpn/vpn-run-root.sh"; if (QFile::exists("vpn/vpn-run-root.sh")) scriptFn = "vpn/vpn-run-root.sh"; auto script = ReadFileText(scriptFn) .replace("$PORT", Int2String(dataStore->inbound_socks_port)) .replace("./nekobox_core", QApplication::applicationDirPath() + "/nekobox_core") .replace("$PROTECT_LISTEN_PATH", protectPath) .replace("$CONFIG_PATH", configPath) .replace("$TABLE_FWMARK", "514"); // write script QFile file2; file2.setFileName(QFileInfo(scriptFn).fileName()); file2.open(QIODevice::ReadWrite | QIODevice::Truncate); file2.write(script.toUtf8()); file2.close(); return QFileInfo(file2).absoluteFilePath(); } }