adapt to sing-box v1.12.0

This commit is contained in:
Nova 2025-08-08 15:03:18 +03:30
parent b522d79619
commit de1b1385c3
6 changed files with 160 additions and 95 deletions

View File

@ -9,7 +9,7 @@ import (
func Check(content []byte) error { func Check(content []byte) error {
ctx := context.Background() ctx := context.Background()
ctx = boxbox.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()) ctx = boxbox.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
options, err := parseConfig(ctx, content) options, err := parseConfig(ctx, content)
if err != nil { if err != nil {
return err return err

View File

@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()) globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
} }

View File

@ -60,6 +60,8 @@ namespace Configs {
void BuildConfigSingBox(const std::shared_ptr<BuildConfigStatus> &status); void BuildConfigSingBox(const std::shared_ptr<BuildConfigStatus> &status);
QJsonObject BuildDnsObject(QString address, bool tunEnabled);
QString BuildChain(int chainId, const std::shared_ptr<BuildConfigStatus> &status); QString BuildChain(int chainId, const std::shared_ptr<BuildConfigStatus> &status);
QString BuildChainInternal(int chainId, const QList<std::shared_ptr<ProxyEntity>> &ents, QString BuildChainInternal(int chainId, const QList<std::shared_ptr<ProxyEntity>> &ents,

View File

@ -33,9 +33,7 @@
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="toolTip"> <property name="toolTip">
<string>outbound.domain_strategy <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Resolve domains to IP before connect, also affects the server address as well&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
when set, domain destinations are resolved to IP before connect,
also if the connection cannot be established with the current address family (ipv4, ipv6), a fallback connection is created shortly after, with the other address family.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Outbound Domain Strategy</string> <string>Outbound Domain Strategy</string>
@ -62,8 +60,7 @@ also if the connection cannot be established with the current address family (ip
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="toolTip"> <property name="toolTip">
<string notr="true">inbound.domain_strategy <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable to resolve domains to IP before routing based on the strategy&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
when used, domain destinations are resolved to IP before routing.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Inbound Domain Strategy</string> <string>Inbound Domain Strategy</string>
@ -82,11 +79,6 @@ when used, domain destinations are resolved to IP before routing.</string>
<string>Sniff result for routing</string> <string>Sniff result for routing</string>
</property> </property>
</item> </item>
<item>
<property name="text">
<string>Sniff result for destination</string>
</property>
</item>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
@ -240,6 +232,9 @@ when used, domain destinations are resolved to IP before routing.</string>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_9"> <widget class="QLabel" name="label_9">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;examples:&lt;br/&gt;tls://8.8.8.8&lt;br/&gt;https://domain/path&lt;/p&gt;&lt;p&gt;tcp://8.8.8.8:1234&lt;/p&gt;&lt;p&gt;dhcp://auto&lt;/p&gt;&lt;p&gt;h3://domain/path&lt;/p&gt;&lt;p&gt;quic://domain:4632&lt;/p&gt;&lt;p&gt;etc&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text"> <property name="text">
<string>Remote DNS</string> <string>Remote DNS</string>
</property> </property>

View File

@ -42,5 +42,5 @@ pushd gen
protoc -I . --go_out=. --protorpc_out=. libcore.proto protoc -I . --go_out=. --protorpc_out=. libcore.proto
popd popd
VERSION_SINGBOX=$(go list -m -f '{{.Version}}' github.com/sagernet/sing-box) VERSION_SINGBOX=$(go list -m -f '{{.Version}}' github.com/sagernet/sing-box)
$GOCMD build -v -o $DEST -trimpath -ldflags "-w -s -X 'github.com/sagernet/sing-box/constant.Version=${VERSION_SINGBOX}'" -tags "with_clash_api,with_gvisor,with_quic,with_wireguard,with_utls,with_ech,with_dhcp" $GOCMD build -v -o $DEST -trimpath -ldflags "-w -s -X 'github.com/sagernet/sing-box/constant.Version=${VERSION_SINGBOX}'" -tags "with_clash_api,with_gvisor,with_quic,with_wireguard,with_utls,with_dhcp"
popd popd

View File

@ -7,9 +7,6 @@
#include <QApplication> #include <QApplication>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#define BOX_UNDERLYING_DNS_EXPORT dataStore->core_box_underlying_dns.isEmpty() ? (status->forExport ? "local" : "underlying://0.0.0.0") : dataStore->core_box_underlying_dns
namespace Configs { namespace Configs {
QString genTunName() { QString genTunName() {
auto tun_name = "throne-tun"; auto tun_name = "throne-tun";
@ -396,8 +393,6 @@ namespace Configs {
} }
// common // common
// apply domain_strategy
outbound["domain_strategy"] = dataStore->routing->outbound_domain_strategy;
// apply mux // apply mux
if (needMux) { if (needMux) {
auto muxObj = QJsonObject{ auto muxObj = QJsonObject{
@ -421,6 +416,88 @@ namespace Configs {
// SingBox // SingBox
QJsonObject BuildDnsObject(QString address, bool tunEnabled)
{
bool usingSystemdResolved = false;
#ifdef Q_OS_LINUX
usingSystemdResolved = ReadFileText("/etc/resolv.conf").contains("systemd-resolved");
#endif
if (address.startsWith("local"))
{
if (tunEnabled && usingSystemdResolved)
{
return {
{"type", "dhcp"}
};
}
return {
{"type", "local"}
};
}
if (address.startsWith("dhcp://"))
{
auto ifcName = address.replace("dhcp://", "");
if (ifcName == "auto") ifcName = "";
return {
{"type", "dhcp"},
{"interface", ifcName},
};
}
QString addr = address;
int port = -1;
QString type = "udp";
QString path = "";
if (address.startsWith("tcp://"))
{
type = "tcp";
addr = addr.replace("tcp://", "");
}
if (address.startsWith("tls://"))
{
type = "tls";
addr = addr.replace("tls://", "");
}
if (address.startsWith("quic://"))
{
type = "quic";
addr = addr.replace("quic://", "");
}
if (address.startsWith("https://"))
{
type = "https";
addr = addr.replace("https://", "");
if (addr.contains("/"))
{
path = addr.split("/").last();
addr = addr.left(addr.indexOf("/"));
}
}
if (address.startsWith("h3://"))
{
type = "h3";
addr = addr.replace("h3://", "");
if (addr.contains("/"))
{
path = addr.split("/").last();
addr = addr.left(addr.indexOf("/"));
}
}
if (addr.contains(":"))
{
auto spl = addr.split(":");
addr = spl[0];
port = spl[1].toInt();
}
QJsonObject res = {
{"type", type},
{"server", addr},
};
if (port != -1) res["server_port"] = port;
if (!path.isEmpty()) res["path"] = path;
return res;
}
void BuildConfigSingBox(const std::shared_ptr<BuildConfigStatus> &status) { void BuildConfigSingBox(const std::shared_ptr<BuildConfigStatus> &status) {
// Prefetch // Prefetch
auto routeChain = profileManager->GetRouteChain(dataStore->routing->current_route_id); auto routeChain = profileManager->GetRouteChain(dataStore->routing->current_route_id);
@ -504,7 +581,6 @@ namespace Configs {
inboundObj["type"] = "mixed"; inboundObj["type"] = "mixed";
inboundObj["listen"] = dataStore->inbound_address; inboundObj["listen"] = dataStore->inbound_address;
inboundObj["listen_port"] = dataStore->inbound_socks_port; inboundObj["listen_port"] = dataStore->inbound_socks_port;
inboundObj["domain_strategy"] = dataStore->routing->domain_strategy;
status->inbounds += inboundObj; status->inbounds += inboundObj;
} }
@ -524,7 +600,6 @@ namespace Configs {
auto tunAddress = QJsonArray{"172.19.0.1/24"}; auto tunAddress = QJsonArray{"172.19.0.1/24"};
if (dataStore->vpn_ipv6) tunAddress += "fdfe:dcba:9876::1/96"; if (dataStore->vpn_ipv6) tunAddress += "fdfe:dcba:9876::1/96";
inboundObj["address"] = tunAddress; inboundObj["address"] = tunAddress;
inboundObj["domain_strategy"] = dataStore->routing->domain_strategy;
if (dataStore->enable_tun_routing && routeChain->defaultOutboundID == proxyID) if (dataStore->enable_tun_routing && routeChain->defaultOutboundID == proxyID)
{ {
if (!directIPCIDRs.isEmpty()) inboundObj["route_exclude_address"] = directIPCIDRs; if (!directIPCIDRs.isEmpty()) inboundObj["route_exclude_address"] = directIPCIDRs;
@ -582,7 +657,6 @@ namespace Configs {
if (!status->forTest) QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray()) if (!status->forTest) QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray())
// Routing // Routing
// geopath
if (NeedGeoAssets()) { if (NeedGeoAssets()) {
status->result->error = "Geo Assets are missing, please download them through Basic Settings -> Assets"; status->result->error = "Geo Assets are missing, please download them through Basic Settings -> Assets";
return; return;
@ -599,15 +673,19 @@ namespace Configs {
} }
if (!status->forTest) routeObj["final"] = outboundIDToString(routeChain->defaultOutboundID); if (!status->forTest) routeObj["final"] = outboundIDToString(routeChain->defaultOutboundID);
if (!dataStore->routing->domain_strategy.isEmpty())
{
auto resolveRule = std::make_shared<RouteRule>();
resolveRule->action = "resolve";
resolveRule->strategy = dataStore->routing->domain_strategy;
resolveRule->inbound = {"mixed-in", "tun-in"};
routeChain->Rules.prepend(resolveRule);
}
if (dataStore->routing->sniffing_mode != SniffingMode::DISABLE) if (dataStore->routing->sniffing_mode != SniffingMode::DISABLE)
{ {
auto sniffRule = std::make_shared<RouteRule>(); auto sniffRule = std::make_shared<RouteRule>();
sniffRule->action = "sniff"; sniffRule->action = "sniff";
sniffRule->inbound = {"mixed-in", "tun-in"}; sniffRule->inbound = {"mixed-in", "tun-in"};
if (dataStore->routing->sniffing_mode == SniffingMode::FOR_DESTINATION)
{
sniffRule->sniffOverrideDest = true;
}
routeChain->Rules.prepend(sniffRule); routeChain->Rules.prepend(sniffRule);
} }
auto neededOutbounds = routeChain->get_used_outbounds(); auto neededOutbounds = routeChain->get_used_outbounds();
@ -645,7 +723,6 @@ namespace Configs {
routeObj["rules"] = routeRules; routeObj["rules"] = routeRules;
// DNS hijack deps // DNS hijack deps
bool needHijackRules = false;
QJsonArray hijackDomains; QJsonArray hijackDomains;
QJsonArray hijackDomainSuffix; QJsonArray hijackDomainSuffix;
QJsonArray hijackDomainRegex; QJsonArray hijackDomainRegex;
@ -665,7 +742,6 @@ namespace Configs {
if (rule.startsWith("regex:")) { if (rule.startsWith("regex:")) {
hijackDomainRegex << rule.mid(6); hijackDomainRegex << rule.mid(6);
} }
needHijackRules = true;
} }
} }
for (auto ruleSet : hijackGeoAssets) { for (auto ruleSet : hijackGeoAssets) {
@ -710,59 +786,74 @@ namespace Configs {
routeObj["rule_set"] = ruleSetArray; routeObj["rule_set"] = ruleSetArray;
// DNS settings // DNS settings
// final add DNS
QJsonObject dns; QJsonObject dns;
QJsonArray dnsServers; QJsonArray dnsServers;
QJsonArray dnsRules; QJsonArray dnsRules;
// Remote // Remote
dnsServers += QJsonObject{ auto remoteDnsObj = BuildDnsObject(dataStore->routing->remote_dns, dataStore->spmode_vpn);
{"tag", "dns-remote"}, remoteDnsObj["tag"] = "dns-remote";
{"address_resolver", "dns-local"}, remoteDnsObj["domain_resolver"] = "dns-local";
{"strategy", dataStore->routing->remote_dns_strategy}, remoteDnsObj["detour"] = tagProxy;
{"address", dataStore->routing->remote_dns}, dnsServers += remoteDnsObj;
{"detour", tagProxy},
};
// Direct // Direct
auto directDNSAddress = dataStore->routing->direct_dns; auto directDNSAddress = dataStore->routing->direct_dns;
if (directDNSAddress == "localhost") directDNSAddress = BOX_UNDERLYING_DNS_EXPORT; auto directDnsObj = BuildDnsObject(directDNSAddress, dataStore->spmode_vpn);
#ifdef Q_OS_LINUX directDnsObj["tag"] = "dns-direct";
auto usingSystemdResolved = ReadFileText("/etc/resolv.conf").contains("systemd-resolved"); directDnsObj["domain_resolver"] = "dns-local";
if (dataStore->spmode_vpn && (directDNSAddress.startsWith("local") || directDNSAddress.startsWith("underlying")) && usingSystemdResolved)
{ // default dns server
MW_show_log("[Warning] Using local dns resolver with systemd-resolved enabled causes a dns loophole, using dhcp://auto as direct dns.");
directDNSAddress = "dhcp://auto";
}
#endif
QJsonObject directObj{
{"tag", "dns-direct"},
{"address_resolver", "dns-local"},
{"strategy", dataStore->routing->direct_dns_strategy},
{"address", directDNSAddress},
{"detour", "direct"},
};
if (dataStore->routing->dns_final_out == "direct") { if (dataStore->routing->dns_final_out == "direct") {
dnsServers.prepend(directObj); dnsServers.prepend(directDnsObj);
} else { } else {
dnsServers.append(directObj); dnsServers.append(directDnsObj);
} }
// block // Handle localhost
dnsServers += QJsonObject{ dnsRules += QJsonObject{
{"tag", "dns-block"}, {"domain", "localhost"},
{"address", "rcode://success"}, {"action", "predefined"},
{"query_type", "A"},
{"rcode", "NOERROR"},
{"answer", "localhost. IN A 127.0.0.1"},
};
dnsRules += QJsonObject{
{"domain", "localhost"},
{"action", "predefined"},
{"query_type", "AAAA"},
{"rcode", "NOERROR"},
{"answer", "localhost. IN AAAA ::1"},
}; };
// Hijack // Hijack
if (dataStore->enable_dns_server && !status->forTest) { if (dataStore->enable_dns_server && !status->forTest) {
dnsServers += QJsonObject { dnsRules += QJsonObject{
{"tag", "dns-hijack"}, {"rule_set", hijackGeoAssets},
{"address", "hijack://10.10.10.10"}, {"domain", hijackDomains},
{"inet4_response", dataStore->dns_v4_resp}, {"domain_suffix", hijackDomainSuffix},
{"inet6_response", dataStore->dns_v6_resp}, {"domain_regex", hijackDomainRegex},
{"query_type", "A"},
{"action", "predefined"},
{"rcode", "NOERROR"},
{"answer", QString("* IN A %1").arg(dataStore->dns_v4_resp)},
}; };
if (!dataStore->dns_v6_resp.isEmpty())
{
dnsRules += QJsonObject{
{"rule_set", hijackGeoAssets},
{"domain", hijackDomains},
{"domain_suffix", hijackDomainSuffix},
{"domain_regex", hijackDomainRegex},
{"query_type", "AAAA"},
{"action", "predefined"},
{"rcode", "NOERROR"},
{"answer", QString("* IN AAAA %1").arg(dataStore->dns_v6_resp)},
};
}
status->inbounds.prepend(QJsonObject{ status->inbounds.prepend(QJsonObject{
{"tag", "dns-in"}, {"tag", "dns-in"},
{"type", "direct"}, {"type", "direct"},
@ -775,22 +866,16 @@ namespace Configs {
if (dataStore->fake_dns) { if (dataStore->fake_dns) {
dnsServers += QJsonObject{ dnsServers += QJsonObject{
{"tag", "dns-fake"}, {"tag", "dns-fake"},
{"address", "fakeip"}, {"type", "fakeip"},
};
dns["fakeip"] = QJsonObject{
{"enabled", true},
{"inet4_range", "198.18.0.0/15"}, {"inet4_range", "198.18.0.0/15"},
{"inet6_range", "fc00::/18"}, {"inet6_range", "fc00::/18"},
}; };
dnsRules += QJsonObject{
{"outbound", "any"},
{"server", "dns-local"},
};
dnsRules += QJsonObject{ dnsRules += QJsonObject{
{"query_type", QJsonArray{ {"query_type", QJsonArray{
"A", "A",
"AAAA" "AAAA"
}}, }},
{"action", "route"},
{"server", "dns-fake"} {"server", "dns-fake"}
}; };
dns["independent_cache"] = true; dns["independent_cache"] = true;
@ -806,34 +891,17 @@ namespace Configs {
{"action", "route"}, {"action", "route"},
{"server", "dns-direct"}, {"server", "dns-direct"},
}; };
} routeObj["default_domain_resolver"] = QJsonObject{
{"server", "dns-direct"},
// dns hijack rules {"strategy", dataStore->routing->outbound_domain_strategy},
if (needHijackRules) {
dnsRules += QJsonObject{
{"rule_set", hijackGeoAssets},
{"domain", hijackDomains},
{"domain_suffix", hijackDomainSuffix},
{"domain_regex", hijackDomainRegex},
{"action", "route"},
{"server", "dns-hijack"},
}; };
} }
// Underlying 100% Working DNS // Underlying 100% Working DNS
auto dnsLocalAddress = BOX_UNDERLYING_DNS_EXPORT; auto dnsLocalAddress = dataStore->core_box_underlying_dns.isEmpty() ? "local" : dataStore->core_box_underlying_dns;
#ifdef Q_OS_LINUX auto dnsLocalObj = BuildDnsObject(dnsLocalAddress, dataStore->spmode_vpn);
if (dataStore->spmode_vpn && (dnsLocalAddress.startsWith("local") || dnsLocalAddress.startsWith("underlying")) && usingSystemdResolved) dnsLocalObj["tag"] = "dns-local";
{ dnsServers += dnsLocalObj;
MW_show_log("[Warning] Using local dns resolver with systemd-resolved enabled causes a dns loophole, using dhcp://auto as local dns.");
dnsLocalAddress = "dhcp://auto";
}
#endif
dnsServers += QJsonObject{
{"tag", "dns-local"},
{"address", dnsLocalAddress},
{"detour", "direct"},
};
dns["servers"] = dnsServers; dns["servers"] = dnsServers;
dns["rules"] = dnsRules; dns["rules"] = dnsRules;
@ -852,7 +920,7 @@ namespace Configs {
{ {
if (dataStore->core_box_clash_api > 0){ if (dataStore->core_box_clash_api > 0){
clash_api = { clash_api = {
{"external_controller", Configs::dataStore->core_box_clash_listen_addr + ":" + Int2String(dataStore->core_box_clash_api)}, {"external_controller", dataStore->core_box_clash_listen_addr + ":" + Int2String(dataStore->core_box_clash_api)},
{"secret", dataStore->core_box_clash_api_secret}, {"secret", dataStore->core_box_clash_api_secret},
{"external_ui", "dashboard"}, {"external_ui", "dashboard"},
}; };