feat: Add dns hijack feature

This commit is contained in:
Nova 2024-09-21 01:58:41 +03:30 committed by Mahdi
parent 6d7ce2868c
commit 41648c0db7
8 changed files with 289 additions and 6 deletions

View File

@ -531,6 +531,17 @@ namespace NekoGui {
{"tag", "dns-out"},
};
if (dataStore->enable_redirect) {
status->inbounds.prepend(QJsonObject{
{"tag", "hijack"},
{"type", "direct"},
{"listen", dataStore->redirect_listen_address},
{"listen_port", dataStore->redirect_listen_port},
{"sniff", true},
{"sniff_override_destination", true},
});
}
// custom inbound
if (!status->forTest) QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray())
@ -579,7 +590,39 @@ namespace NekoGui {
status->domainListDNSDirect << neededEnt->bean->serverAddress;
}
}
routeObj["rules"] = routeChain->get_route_rules(false, outboundMap);
auto routeRules = routeChain->get_route_rules(false, outboundMap);
if (dataStore->enable_dns_server) routeRules.prepend(QJsonObject{
{"inbound", "dns-in"},
{"outbound", "dns-out"}
});
routeObj["rules"] = routeRules;
bool needHijackRules = false;
QJsonArray hijackDomains;
QJsonArray hijackDomainSuffix;
QJsonArray hijackDomainRegex;
QJsonArray hijackGeoAssets;
if (dataStore->enable_dns_server) {
for (const auto& rule : dataStore->dns_server_rules) {
if (rule.startsWith("ruleset:")) {
hijackGeoAssets << rule.mid(8);
}
if (rule.startsWith("domain:")) {
hijackDomains << rule.mid(7);
}
if (rule.startsWith("suffix:")) {
hijackDomainSuffix << rule.mid(7);
}
if (rule.startsWith("regex:")) {
hijackDomainRegex << rule.mid(6);
}
needHijackRules = true;
}
}
for (auto ruleSet : hijackGeoAssets) {
if (!neededRuleSets->contains(ruleSet.toString())) neededRuleSets->append(ruleSet.toString());
}
auto ruleSetArray = QJsonArray();
for (const auto &item: *neededRuleSets) {
@ -637,6 +680,24 @@ namespace NekoGui {
{"address", "rcode://success"},
};
// Hijack
if (dataStore->enable_dns_server) {
dnsServers += QJsonObject {
{"tag", "dns-hijack"},
{"address", "hijack://10.10.10.10"},
{"inet4_response", dataStore->dns_v4_resp},
{"inet6_response", dataStore->dns_v6_resp},
};
status->inbounds.prepend(QJsonObject{
{"tag", "dns-in"},
{"type", "direct"},
{"listen", dataStore->dns_server_listen_addr},
{"listen_port", dataStore->dns_server_listen_port},
{"sniff", true},
});
}
// Fakedns
if (dataStore->fake_dns) {
dnsServers += QJsonObject{
@ -706,6 +767,17 @@ namespace NekoGui {
};
}
// dns hijack rules
if (needHijackRules) {
dnsRules += QJsonObject{
{"rule_set", hijackGeoAssets},
{"domain", hijackDomains},
{"domain_suffix", hijackDomainSuffix},
{"domain_regex", hijackDomainRegex},
{"server", "dns-hijack"},
};
}
// Underlying 100% Working DNS
dnsServers += QJsonObject{
{"tag", "dns-local"},

View File

@ -10,7 +10,9 @@ require (
grpc_server v1.0.0
)
replace github.com/sagernet/sing-box => github.com/Mahdi-zarei/sing-box v1.3.5-0.20240918061449-35f1c345ec90
replace github.com/sagernet/sing-box => github.com/Mahdi-zarei/sing-box v1.3.5-0.20240920073315-bc73938972f4
replace github.com/sagernet/sing-dns => github.com/Mahdi-zarei/sing-dns v0.3.0-beta.14.0.20240918175353-709eafff43d3
require (
berty.tech/go-libtor v1.0.385 // indirect

View File

@ -4,10 +4,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Mahdi-zarei/sing-box v1.3.5-0.20240918061449-35f1c345ec90 h1:I1ONdswCt5excwjoCcpIRRGOFkc/+TXfxynNVS95iqk=
github.com/Mahdi-zarei/sing-box v1.3.5-0.20240918061449-35f1c345ec90/go.mod h1:nRI5VekCi5MxQyJyW6AcWiXcmjjW6hX8eJMhV78T/Fo=
github.com/Mahdi-zarei/sing-box v1.3.5-0.20240920073315-bc73938972f4 h1:8joOa6IehQSy5wKnTN2YBuvwKZf0sfSvdU1dC2hCeJ8=
github.com/Mahdi-zarei/sing-box v1.3.5-0.20240920073315-bc73938972f4/go.mod h1:MSnb8j3iBcjvuf/w7SfApa1WgBC6XecxhBeLt1vzDJk=
github.com/Mahdi-zarei/sing-box-extra v0.0.0-20240918062427-6045c5730e82 h1:h09DnPUhOGfhg7I3aJJehkjo/oC2QH4APxnWjxk/wZo=
github.com/Mahdi-zarei/sing-box-extra v0.0.0-20240918062427-6045c5730e82/go.mod h1:5/Y7uNbJpofpogdnG5Ao/Yz+bjnA8cOU1nInfA+tV4s=
github.com/Mahdi-zarei/sing-dns v0.3.0-beta.14.0.20240918175353-709eafff43d3 h1:1YoZLwSYHOZ9J9DftJrI/FADEjv29kPptn5g34A3vC4=
github.com/Mahdi-zarei/sing-dns v0.3.0-beta.14.0.20240918175353-709eafff43d3/go.mod h1:rscgSr5ixOPk8XM9ZMLuMXCyldEQ1nLvdl0nfv+lp00=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
@ -152,8 +154,6 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4Wk
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.0-beta.2 h1:V12EpwtsgYo5OLGjAiGoJobDJZeUsKv0b5y+yGAM6W0=
github.com/sagernet/sing v0.5.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-dns v0.3.0-beta.14 h1:/s+fJzYKsvLaNDt/2rjpsrDcN8wmCO2JbX6OFrl8Nww=
github.com/sagernet/sing-dns v0.3.0-beta.14/go.mod h1:rscgSr5ixOPk8XM9ZMLuMXCyldEQ1nLvdl0nfv+lp00=
github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo=
github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
github.com/sagernet/sing-quic v0.3.0-beta.3 h1:8S98VXZxtSiOqVCFbCNbMEvKDPhOF/VNBYMjVC3xMhw=

View File

@ -295,6 +295,15 @@ namespace NekoGui {
_add(new configItem("ntp_interval", &ntp_interval, itemType::string));
_add(new configItem("geoip_download_url", &geoip_download_url, itemType::string));
_add(new configItem("geosite_download_url", &geosite_download_url, itemType::string));
_add(new configItem("enable_dns_server", &enable_dns_server, itemType::boolean));
_add(new configItem("dns_server_listen_addr", &dns_server_listen_addr, itemType::string));
_add(new configItem("dns_server_listen_port", &dns_server_listen_port, itemType::integer));
_add(new configItem("dns_v4_resp", &dns_v4_resp, itemType::string));
_add(new configItem("dns_v6_resp", &dns_v6_resp, itemType::string));
_add(new configItem("dns_server_rules", &dns_server_rules, itemType::stringList));
_add(new configItem("enable_redirect", &enable_redirect, itemType::boolean));
_add(new configItem("redirect_listen_address", &redirect_listen_address, itemType::string));
_add(new configItem("redirect_listen_port", &redirect_listen_port, itemType::integer));
}
void DataStore::UpdateStartedId(int id) {

View File

@ -134,6 +134,21 @@ namespace NekoGui {
int ntp_server_port = 0;
QString ntp_interval = "";
// Hijack
bool enable_dns_server = false;
QString dns_server_listen_addr = "127.0.0.1";
#ifdef Q_OS_LINUX
int dns_server_listen_port = 5353;
#else
int dns_server_listen_port = 53;
#endif
QString dns_v4_resp = "127.0.0.1";
QString dns_v6_resp = "::1";
QStringList dns_server_rules = {};
bool enable_redirect = false;
QString redirect_listen_address = "127.0.0.1";
int redirect_listen_port = 443;
// Hotkey
QString hotkey_mainwindow = "";
QString hotkey_group = "";

View File

@ -38,6 +38,22 @@ void DialogManageRoutes::reloadProfileItems() {
blocker.unblock();
}
void DialogManageRoutes::set_dns_hijack_enability(const bool enable) const {
ui->dnshijack_listenaddr->setEnabled(enable);
ui->dnshijack_listenport->setEnabled(enable);
ui->dnshijack_rules->setEnabled(enable);
ui->dnshijack_v4resp->setEnabled(enable);
ui->dnshijack_v6resp->setEnabled(enable);
}
bool DialogManageRoutes::validate_dns_rules(const QString &rawString) {
auto rules = rawString.split("\n");
for (const auto& rule : rules) {
if (!rule.trimmed().isEmpty() && !rule.startsWith("ruleset:") && !rule.startsWith("domain:") && !rule.startsWith("suffix:") && !rule.startsWith("regex:")) return false;
}
return true;
}
DialogManageRoutes::DialogManageRoutes(QWidget *parent) : QDialog(parent), ui(new Ui::DialogManageRoutes) {
ui->setupUi(this);
auto profiles = NekoGui::profileManager->routes;
@ -107,6 +123,35 @@ DialogManageRoutes::DialogManageRoutes(QWidget *parent) : QDialog(parent), ui(ne
on_delete_route_clicked();
});
// hijack
ui->dnshijack_enable->setChecked(NekoGui::dataStore->enable_dns_server);
set_dns_hijack_enability(NekoGui::dataStore->enable_dns_server);
ui->dnshijack_listenaddr->setText(NekoGui::dataStore->dns_server_listen_addr);
ui->dnshijack_listenport->setValidator(QRegExpValidator_Number);
ui->dnshijack_listenport->setText(Int2String(NekoGui::dataStore->dns_server_listen_port));
ui->dnshijack_rules->setText(NekoGui::dataStore->dns_server_rules.join("\n"));
ui->dnshijack_v4resp->setText(NekoGui::dataStore->dns_v4_resp);
ui->dnshijack_v6resp->setText(NekoGui::dataStore->dns_v6_resp);
#ifndef Q_OS_LINUX
ui->dnshijack_listenport->setVisible(false);
ui->dnshijack_listenport_l->setVisible(false);
#endif
ui->redirect_enable->setChecked(NekoGui::dataStore->enable_redirect);
ui->redirect_listenaddr->setEnabled(NekoGui::dataStore->enable_redirect);
ui->redirect_listenaddr->setText(NekoGui::dataStore->redirect_listen_address);
ui->redirect_listenport->setEnabled(NekoGui::dataStore->enable_redirect);
ui->redirect_listenport->setValidator(QRegExpValidator_Number);
ui->redirect_listenport->setText(Int2String(NekoGui::dataStore->redirect_listen_port));
connect(ui->dnshijack_enable, &QCheckBox::checkStateChanged, this, [=](bool state) {
set_dns_hijack_enability(state);
});
connect(ui->redirect_enable, &QCheckBox::checkStateChanged, this, [=](bool state) {
ui->redirect_listenaddr->setEnabled(state);
ui->redirect_listenport->setEnabled(state);
});
ADD_ASTERISK(this)
}
@ -123,6 +168,10 @@ void DialogManageRoutes::accept() {
MessageBoxInfo("Invalid settings", "Routing profile cannot be empty");
return;
}
if (!validate_dns_rules(ui->dnshijack_rules->toPlainText())) {
MessageBoxInfo("Invalid settings", "DNS Rules are not valid");
return;
}
NekoGui::dataStore->routing->sniffing_mode = ui->sniffing_mode->currentIndex();
NekoGui::dataStore->routing->domain_strategy = ui->domainStrategyCombo->currentText();
@ -140,6 +189,22 @@ void DialogManageRoutes::accept() {
NekoGui::dataStore->routing->current_route_id = currentRouteProfileID;
NekoGui::dataStore->routing->def_outbound = ui->default_out->currentText();
NekoGui::dataStore->enable_dns_server = ui->dnshijack_enable->isChecked();
NekoGui::dataStore->dns_server_listen_addr = ui->dnshijack_listenaddr->text();
NekoGui::dataStore->dns_server_listen_port = ui->dnshijack_listenport->text().toInt();
NekoGui::dataStore->dns_v4_resp = ui->dnshijack_v4resp->text();
NekoGui::dataStore->dns_v6_resp = ui->dnshijack_v6resp->text();
auto rawRules = ui->dnshijack_rules->toPlainText().split("\n");
QStringList dnsRules;
for (const auto& rawRule : rawRules) {
if (rawRule.trimmed().isEmpty()) continue;
dnsRules.append(rawRule.trimmed());
}
NekoGui::dataStore->dns_server_rules = dnsRules;
NekoGui::dataStore->enable_redirect = ui->redirect_enable->isChecked();
NekoGui::dataStore->redirect_listen_address = ui->redirect_listenaddr->text();
NekoGui::dataStore->redirect_listen_port = ui->redirect_listenport->text().toInt();
//
QStringList msg{"UpdateDataStore"};

View File

@ -32,6 +32,10 @@ private:
int currentRouteProfileID = -1;
void set_dns_hijack_enability(bool enable) const;
static bool validate_dns_rules(const QString &rawString);
QShortcut* deleteShortcut;
public slots:
void accept() override;

View File

@ -132,6 +132,122 @@ also if the connection cannot be established with the current address family (ip
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>Hijack</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>DNS Server</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="dnshijack_enable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="dnshijack_what">
<property name="text">
<string>How does it work?</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="dnshijack_listenaddr_l">
<property name="text">
<string>Listen Address</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="dnshijack_listenaddr"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="dnshijack_listenport_l">
<property name="text">
<string>Listen Port</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="dnshijack_listenport"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="dnshijack_rules_l">
<property name="text">
<string>Rules</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QTextEdit" name="dnshijack_rules"/>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="dnshijack_v4resp"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="dnshijack_v4resp_l">
<property name="text">
<string>IPv4 Response</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="dnshijack_v6resp"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="dnshijack_v6resp_l">
<property name="text">
<string>IPv6 Response</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Redirect Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="2" column="1">
<widget class="QLineEdit" name="redirect_listenport"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="redirect_listenport_l">
<property name="text">
<string>Listen Port</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="redirect_enable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="redirect_listenaddr"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="redirect_listenaddr_l">
<property name="text">
<string>Listen Address</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>DNS</string>