+
+
+define Package/luci-app-netwizard
+ CATEGORY:=X
+ SUBMENU:=Configuration netWizard Support
+ TITLE:=LuCI Support for netwizard
+ PKGARCH:=all
+endef
+
+define Package/luci-app-netwizard/description
+ LuCI Support for netwizard.
+endef
+define Package/luci-app-netwizard/conffiles
+/etc/config/netwizard
+endef
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/netwizard/netwizard.js b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/netwizard/netwizard.js
new file mode 100644
index 0000000000..daba7ecbe5
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/netwizard/netwizard.js
@@ -0,0 +1,1228 @@
+// Copyright 2023-2026 sirpdboy
+'use strict';
+'require view';
+'require form';
+'require network';
+'require uci';
+'require validation';
+'require rpc';
+'require fs';
+'require dom';
+'require poll';
+'require tools.widgets as widgets';
+
+var callExecRPC = rpc.declare({
+ object: 'file',
+ method: 'exec',
+ params: ['command', 'params'],
+ expect: { '': {} }
+});
+
+
+return view.extend({
+ load: function() {
+ return Promise.all([
+ network.getDevices(),
+ uci.changes(),
+ L.resolveDefault(uci.load('wireless'), null),
+ uci.load('network'),
+ uci.load('netwizard')
+ ]);
+ },
+
+ render: function(data) {
+ var devices = data[0] || [];
+ var has_wifi = false;
+ var m, o, s;
+
+ try {
+ var wirelessSections = uci.sections('wireless', 'wifi-device');
+ if (wirelessSections && wirelessSections.length > 0) {
+ has_wifi = true;
+ } else {
+ var wifiIfaces = uci.sections('wireless', 'wifi-iface');
+ if (wifiIfaces && wifiIfaces.length > 0) {
+ has_wifi = true;
+ }
+ }
+ } catch (e) {
+ has_wifi = false;
+ }
+
+ var physicalIfaces = 0;
+ var physicalInterfaces = [];
+
+ for (var i = 0; i < devices.length; i++) {
+ var iface = devices[i].getName();
+ if (!iface.match(/_ifb$/) && !iface.match(/^ifb/) &&
+ !iface.match(/^veth/) && !iface.match(/^tun/) &&
+ !iface.match(/^tap/) && !iface.match(/^gre/) &&
+ !iface.match(/^gretap/) && !iface.match(/^lo$/) &&
+ !iface.match(/^br-/) &&
+ (iface.match(/^(eth|en|usb)/) || iface.match(/^wlan|^wl/))) {
+
+ physicalIfaces++;
+ physicalInterfaces.push(iface);
+ }
+ }
+
+ var lan_ip = uci.get('netwizard', 'default', 'lan_ipaddr');
+ var lan_mask = uci.get('netwizard', 'default', 'lan_netmask');
+ var wan_face = uci.get('netwizard', 'default', 'wan_interface');
+ var wanproto = uci.get('netwizard', 'default', 'wan_proto');
+ var LanHTTPS = uci.get('netwizard', 'default', 'https') || '0';
+
+ if (!lan_ip) {
+ lan_ip = uci.get('network', 'lan', 'ipaddr') || '192.168.10.1/24' ;
+ lan_ip = (lan_ip + '');
+ if (lan_ip.indexOf('/') > -1) {
+ lan_ip = lan_ip.split('/')[0];
+ }
+ }
+
+ if (!lan_mask) {
+ lan_mask = uci.get('network', 'lan', 'netmask') || '255.255.255.0' ;
+ }
+
+ if (!wan_face) {
+ wan_face = uci.get('network', 'wan', 'device') || 'eth1';
+ }
+
+ if (!wanproto) {
+ wanproto = uci.get('network', 'wan', 'proto') || 'siderouter';
+ }
+
+ this.devices = devices;
+ this.has_wifi = has_wifi;
+ this.physicalIfaces = physicalIfaces;
+ this.physicalInterfaces = physicalInterfaces;
+ this.lan_mask = lan_mask;
+ this.lan_ip = lan_ip;
+ this.LanHTTPS = LanHTTPS;
+ this.wan_face = wan_face;
+ this.wanproto = wanproto;
+
+ this.addStyles();
+
+ var params = new URLSearchParams(window.location.search);
+ var selectedMode = params.get('selectedMode');
+
+ if (selectedMode) {
+ return this.renderConfigForm(selectedMode);
+ } else {
+ return this.renderModeSelection();
+ }
+ },
+
+ addStylesnobnt: function() {
+ if (document.getElementById('netwizard-mode-styles-nobnt')) {
+ return;
+ }
+
+ var stylen = E('style', { 'id': 'netwizard-mode-styles-nobnt' }, `
+ #view .cbi-page-actions {
+ display: none;
+ }
+ `);
+
+ document.head.appendChild(stylen);
+ },
+
+ addStyles: function() {
+ if (document.getElementById('netwizard-mode-styles')) {
+ return;
+ }
+
+ var style = E('style', { 'id': 'netwizard-mode-styles' }, `
+ .mode-selection-container {
+ margin-top: 5rem;
+ padding: 1rem;
+ }
+
+ .mode-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ margin: 30px 0;
+ justify-content: center;
+ }
+
+ .mode-card {
+ border-radius: 8px;
+ padding: 4rem 1rem;
+ cursor: pointer;
+ transition: all 0.3s;
+ text-align: center;
+ flex: 1;
+ min-width: 180px;
+ max-width: 180px;
+ box-shadow: 0 0.3rem 0.5rem var(--input-boxcolor);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 2px solid transparent;
+ }
+
+ .mode-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+ }
+
+ .mode-card[data-mode="pppoe"] {
+ background: rgba(255,107,107,0.7);
+ border-color: rgba(255,107,107,0.7);
+ color: white;
+ }
+
+ .mode-card[data-mode="pppoe"]:hover {
+ border-color: #ff3838;
+ box-shadow: 0 4px 12px rgba(255, 71, 87, 0.3);
+ }
+
+ .mode-card[data-mode="dhcp"] {
+ background: rgba(51,154,240,0.7);
+ border-color: rgba(51,154,240,0.7);
+ color: white;
+ }
+
+ .mode-card[data-mode="dhcp"]:hover {
+ border-color: #01b7ff;
+ box-shadow: 0 4px 12px rgba(34, 139, 230, 0.3);
+ }
+
+ .mode-card[data-mode="siderouter"] {
+ background: rgba(81,207,102,0.7);
+ border-color: rgba(81,207,102,0.7);
+ color: white;
+ }
+
+ .mode-card[data-mode="siderouter"]:hover {
+ border-color: #27f94d;
+ box-shadow: 0 4px 12px rgba(64, 192, 87, 0.3);
+ }
+
+ .mode-icon-container {
+ width: 64px;
+ height: 64px;
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255,1);
+ border-radius: 10%;
+ padding: 10px;
+ box-shadow: 0 0.3rem 0.5rem rgba(0,0,0,0.22);
+ }
+
+ .mode-icon {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+ }
+
+ .mode-title {
+ font-size: 16px;
+ font-weight: 600;
+ margin-top: 10px;
+ text-align: center;
+ }
+
+ .mode-description {
+ font-size: 13px;
+ line-height: 1.4;
+ margin-bottom: 15px;
+ min-height: 60px;
+ text-align: center;
+ opacity: 0.9;
+ }
+
+ .quick-nav-buttons {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ margin: 20px;
+ flex-wrap: wrap;
+ }
+
+ .quick-nav-btn {
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ line-height: 1rem;
+ cursor: pointer;
+ transition: background 0.3s;
+ text-decoration: none;
+ display: inline-block;
+ }
+
+ .mode-info-header {
+ border-radius: 8px;
+ padding: 1rem;
+ margin: 0 2% 2% 2%;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ }
+
+ .mode-info-content {
+ flex: 1;
+ }
+
+ .mode-info-header[data-mode="pppoe"] {
+ background: rgba(255,107,107,0.7);
+ }
+
+ .mode-info-header[data-mode="dhcp"] {
+ background: rgba(51,154,240,0.7);
+ }
+
+ .mode-info-header[data-mode="siderouter"] {
+ background: rgba(81,207,102,0.7);
+ }
+
+ @media (max-width: 768px) {
+ .mode-selection-container {
+ margin-top: 0;
+ padding: 0;
+ }
+ .mode-grid {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .mode-card {
+ min-width: 90%;
+ max-width: 90%;
+ }
+
+ .quick-nav-buttons {
+ flex-direction: column;
+ }
+
+ .quick-nav-btn {
+ width: 90%;
+ text-align: center;
+ }
+
+ .mode-info-header {
+ flex-direction: column;
+ text-align: center;
+ }
+ }
+ `);
+
+ document.head.appendChild(style);
+ },
+
+ getModeIcon: function(mode) {
+ var svgCode;
+ var color = this.getModeColor(mode);
+
+ switch(mode) {
+ case 'pppoe':
+ svgCode = ` `;
+ break;
+ case 'dhcp':
+ svgCode = `
+
+ `;
+ break;
+ case 'siderouter':
+ svgCode = `
+
+ `;
+ break;
+ }
+
+ var svgUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgCode)));
+ return ' ';
+ },
+
+ getModeTitle: function(mode) {
+ switch(mode) {
+ case 'pppoe': return _('PPPoE Dial-up');
+ case 'dhcp': return _('DHCP Client');
+ case 'siderouter': return _('Side Router');
+ default: return _('WAN Settings');
+ }
+ },
+
+ getModeDescription: function(mode) {
+ switch(mode) {
+ case 'pppoe': return _('Fiber broadband that requires username/password');
+ case 'dhcp': return _('Connect to router as a subordinate router to internet');
+ case 'siderouter': return _('Configure as side router in same network as main router');
+ default: return _('Network connection mode');
+ }
+ },
+
+ getModeColor: function(mode) {
+ switch(mode) {
+ case 'pppoe': return '#ff6b6b';
+ case 'dhcp': return '#339af0';
+ case 'siderouter': return '#51cf66';
+ default: return '#36c';
+ }
+ },
+
+ renderModeSelection: function() {
+ var container = E('div', { 'class': 'mode-selection-container' }, [
+ E('h2', { 'style': 'margin-top: 4%;margin-bottom: 15px;text-align: center;padding: 1rem;font-size: 1.8rem;font-weight: 600;' },
+ _('Select Network Connection Mode')),
+ E('p', { 'style': 'margin-bottom: 1rem;text-align: center;font-size: 1.1rem;' },
+ _('Choose the connection mode that matches your network environment'))
+ ]);
+
+ var modeGrid = E('div', { 'class': 'mode-grid' });
+
+ var modes = [
+ { id: 'pppoe' },
+ { id: 'dhcp' },
+ { id: 'siderouter' }
+ ];
+
+ var self = this;
+ modes.forEach(function(mode) {
+ var iconDiv = E('div', {
+ 'class': 'mode-icon-container'
+ });
+
+ iconDiv.innerHTML = self.getModeIcon(mode.id);
+
+ var card = E('div', {
+ 'class': 'mode-card',
+ 'data-mode': mode.id
+ }, [
+ iconDiv,
+ E('div', { 'class': 'mode-title' }, self.getModeTitle(mode.id)),
+ ]);
+
+ card.addEventListener('click', function() {
+ self.selectMode(mode.id);
+ });
+ self.addStylesnobnt();
+ modeGrid.appendChild(card);
+ });
+
+ container.appendChild(modeGrid);
+ return container;
+ },
+
+ selectMode: function(mode) {
+ uci.set('netwizard', 'default', 'wan_proto', mode);
+ uci.save();
+ var currentUrl = window.location.pathname;
+ var newUrl = currentUrl + '?selectedMode=' + mode + '&tab=wansetup';
+ window.location.href = newUrl;
+ },
+
+ renderConfigForm: function(selectedMode) {
+ var wanproto = selectedMode || this.wanproto;
+
+ var m = new form.Map('netwizard', _('Quick Network Setup Wizard'),
+ _('Quick network setup wizard. If you need more settings, please enter network - interface to set.'));
+
+ var s = m.section(form.NamedSection, 'default');
+ s.addremove = false;
+ s.anonymous = true;
+
+ s.tab('modesetup', _('Network Mode'));
+ s.tab('wansetup', _('WAN Settings'));
+ if (this.has_wifi) {
+ s.tab('wifisetup', _('Wireless Settings'), _('Set the router\'s wireless name and password. For more advanced settings, please go to the Network-Wireless page.'));
+ }
+ s.tab('othersetup', _('Other Settings'));
+
+ var modeTitle = this.getModeTitle(wanproto);
+ var modeIcon = this.getModeIcon(wanproto);
+ var modeDescription = this.getModeDescription(wanproto);
+ var modeColor = this.getModeColor(wanproto);
+
+ var o = s.taboption('modesetup', form.DummyValue, 'current_mode', _('Current Network Mode'));
+ o.rawhtml = true;
+ o.default = '' +
+ '
' + modeIcon + '
' +
+ '
' + modeTitle + ' ' +
+ '
' + modeDescription + '
' +
+ '
' +
+ '
';
+
+ var modeInfoHeader = s.taboption('wansetup', form.DummyValue, 'mode_info_header', '');
+ modeInfoHeader.rawhtml = true;
+ modeInfoHeader.default = '';
+
+ o = s.taboption('modesetup', form.ListValue, 'wan_proto', _('Protocol'),
+ _('Three different ways to access the Internet, please choose according to your own situation.'));
+ o.default = wanproto;
+ o.value('dhcp', _('DHCP Client'));
+ o.value('pppoe', _('PPPoE Dial-up'));
+ o.value('siderouter', _('Side Router'));
+ o.rmempty = false;
+ o.readonly = true;
+
+ o = s.taboption('wansetup', form.Flag, 'setlan', _('Add LAN port configuration'));
+ o.depends('wan_proto', 'pppoe');
+ o.depends('wan_proto', 'dhcp');
+ o.default = 0;
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.ListValue, 'lan_proto', _('LAN IP Address Mode'),
+ _('Warning: Setting up automatic IP address retrieval requires checking the IP address on the higher-level router'));
+ o.default = 'static';
+ o.value('static', _('Static IP address (Specify non conflicting IP addresses)'));
+ o.value('dhcp', _('DHCP client (Main router assigns IP)'));
+ o.depends('wan_proto', 'siderouter');
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'lan_ipaddr', _('LAN IPv4 Address'),
+ _('You must specify the IP address of this machine, which is the IP address of the web access route'));
+ o.default = this.lan_ip;
+ o.datatype = 'ip4addr';
+ o.rmempty = false;
+ o.depends({'wan_proto':'pppoe','setlan': '1'});
+ o.depends({'wan_proto': 'dhcp' ,'setlan': '1'});
+ o.depends({'wan_proto': 'siderouter', 'lan_proto': 'static' });
+
+ o = s.taboption('wansetup', form.Value, 'lan_netmask', _('LAN IPv4 Netmask'));
+ o.datatype = 'ip4addr';
+ o.value('255.255.255.0');
+ o.value('255.255.0.0');
+ o.value('255.0.0.0');
+ o.default = this.lan_mask;
+ o.depends({'wan_proto': 'siderouter', 'lan_proto': 'static'});
+ o.depends({'wan_proto': 'pppoe','setlan': '1'});
+ o.depends({'wan_proto': 'dhcp','setlan': '1'});
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'lan_gateway', _('LAN IPv4 Gateway'),
+ _('Please enter the main routing IP address. The bypass gateway is not the same as the login IP of this bypass WEB and is in the same network segment'));
+ o.depends({'wan_proto': 'siderouter', 'lan_proto': 'static'});
+ o.datatype = 'ip4addr';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.ListValue, 'dhcp_proto', _('WAN interface IP address mode'),
+ _('Choose how to get IP address for WAN interface'));
+ o.default = 'dhcp';
+ o.value('static', _('Static IP address (Specify non conflicting IP addresses)'));
+ o.value('dhcp', _('DHCP client (existing router assigns IP)'));
+ o.depends('wan_proto', 'dhcp');
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.DynamicList, 'lan_dns', _('Use Custom SideRouter DNS'));
+ o.value('223.5.5.5', _('Ali DNS: 223.5.5.5'));
+ o.value('180.76.76.76', _('Baidu DNS: 180.76.76.76'));
+ o.value('114.114.114.114', _('114 DNS: 114.114.114.114'));
+ o.value('8.8.8.8', _('Google DNS: 8.8.8.8'));
+ o.value('1.1.1.1', _('Cloudflare DNS: 1.1.1.1'));
+ o.depends({'wan_proto': 'siderouter'});
+ o.datatype = 'ip4addr';
+ o.default = '223.5.5.5';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', widgets.DeviceSelect, 'wan_interface',
+ _('Device'),
+ _('Allocate the physical interface of WAN port'));
+ o.depends({'wan_proto': 'pppoe','setlan': '1'});
+ o.depends({'wan_proto': 'dhcp','setlan': '1'});
+ o.default = this.wan_face;
+ o.ucioption = 'wan_interface';
+ o.nobridges = false;
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'wan_pppoe_user', _('PAP/CHAP Username'));
+ o.depends('wan_proto', 'pppoe');
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'wan_pppoe_pass', _('PAP/CHAP Password'));
+ o.depends('wan_proto', 'pppoe');
+ o.password = true;
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'wan_ipaddr', _('WAN IPv4 Address'));
+ o.depends({'wan_proto': 'dhcp', 'dhcp_proto': 'static'});
+ o.datatype = 'ip4addr';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'wan_netmask', _('WAN IPv4 Netmask'));
+ o.depends({'wan_proto': 'dhcp', 'dhcp_proto': 'static'});
+ o.datatype = 'ip4addr';
+ o.value('255.255.255.0');
+ o.value('255.255.0.0');
+ o.value('255.0.0.0');
+ o.default = '255.255.255.0';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Value, 'wan_gateway', _('WAN IPv4 Gateway'));
+ o.depends({'wan_proto': 'dhcp', 'dhcp_proto': 'static'});
+ o.datatype = 'ip4addr';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.DynamicList, 'wan_dns', _('Use custom DNS servers'));
+ o.value('', _('Auto-fetch'));
+ o.value('223.5.5.5', _('Ali DNS: 223.5.5.5'));
+ o.value('180.76.76.76', _('Baidu DNS: 180.76.76.76'));
+ o.value('114.114.114.114', _('114 DNS: 114.114.114.114'));
+ o.value('8.8.8.8', _('Google DNS: 8.8.8.8'));
+ o.value('1.1.1.1', _('Cloudflare DNS: 1.1.1.1'));
+ o.depends({'wan_proto': 'dhcp'});
+ o.depends('wan_proto', 'pppoe');
+ o.datatype = 'ip4addr';
+
+ o = s.taboption('wansetup', form.Flag, 'ipv6', _('Enable IPv6'));
+ o.default = '0';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Flag, 'lan_dhcp', _('Disable DHCP Server'),
+ _('Selecting means that the DHCP server is not enabled. In a network, only one DHCP server is needed to allocate and manage client IPs. If it is a siderouter route, it is recommended to turn off the primary routing DHCP server.'));
+ o.default = '0';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Flag, 'dnsset', _('Enable DNS Notifications (IPv4/IPv6)'),
+ _('Forcefully specify the DNS server for this router'));
+ o.depends('lan_dhcp', '0');
+ o.default = '0';
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.ListValue, 'dns_tables', _('Use custom DNS servers'));
+ o.value('1', _('Use local IP for DNS (default)'));
+ o.value('223.5.5.5', _('Ali DNS: 223.5.5.5'));
+ o.value('180.76.76.76', _('Baidu DNS: 180.76.76.76'));
+ o.value('114.114.114.114', _('114 DNS: 114.114.114.114'));
+ o.value('8.8.8.8', _('Google DNS: 8.8.8.8'));
+ o.value('1.1.1.1', _('Cloudflare DNS: 1.1.1.1'));
+ o.depends('dnsset', '1');
+ o.rmempty = false;
+
+ o = s.taboption('wansetup', form.Flag, 'https', _('Redirect to HTTPS'),
+ _('Enable automatic redirection of HTTP requests to HTTPS port.'));
+ o.default = '0';
+ o.rmempty = false;
+
+ if (this.has_wifi) {
+ var wifi_ssid = s.taboption('wifisetup', form.Value, 'wifi_ssid', _('ESSID '));
+ wifi_ssid.datatype = 'maxlength(32)';
+
+ var wifi_key = s.taboption('wifisetup', form.Value, 'wifi_key', _('Key'));
+ wifi_key.datatype = 'wpakey';
+ wifi_key.password = true;
+ }
+
+ o = s.taboption('othersetup', form.Flag, 'synflood', _('Enable SYN-flood Defense'),
+ _('Enable Firewall SYN-flood defense [Suggest opening]'));
+ o.default = '1';
+ o.rmempty = false;
+
+ o = s.taboption('othersetup', form.Flag, 'updatacheck', _('Enable detection update prompts'));
+ o.default = '0';
+ o.rmempty = false;
+
+ var originalSave = m.save;
+ var currentLanIP = this.lan_ip;
+ var currentHTTPS = this.LanHTTPS;
+ var self = this;
+
+ function getNewLanIP() {
+ var selectors = [
+ 'input[id="widget.cbid.netwizard.default.lan_ipaddr"]',
+ 'input[name="widget.cbid.netwizard.default.lan_ipaddr"]',
+ 'input[data-option="lan_ipaddr"]',
+ 'input[placeholder*="IP"]',
+ '.cbi-input-text[type="text"]'
+ ];
+
+ for (var i = 0; i < selectors.length; i++) {
+ var inputs = document.querySelectorAll(selectors[i]);
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (input && input.value) {
+ var ipMatch = input.value.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
+ if (ipMatch) {
+ var valid = true;
+ for (var k = 1; k <= 4; k++) {
+ var part = parseInt(ipMatch[k]);
+ if (part < 0 || part > 255) {
+ valid = false;
+ break;
+ }
+ }
+ if (valid) {
+ return input.value;
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function getLanproto() {
+ return new Promise(function(resolve, reject) {
+ try {
+ var selectors = [
+ 'select[id="widget.cbid.netwizard.default.lan_proto"]',
+ 'select[name="widget.cbid.netwizard.default.lan_proto"]'
+ ];
+
+ for (var i = 0; i < selectors.length; i++) {
+ var selects = document.querySelectorAll(selectors[i]);
+ for (var j = 0; j < selects.length; j++) {
+ var select = selects[j];
+ if (select && select.value) {
+ resolve(select.value === 'dhcp');
+ return;
+ }
+ }
+ }
+
+ var lanProtoConfig = uci.get('netwizard', 'default', 'lan_proto');
+ if (lanProtoConfig) {
+ resolve(lanProtoConfig === 'dhcp');
+ return;
+ }
+
+ resolve(false);
+
+ } catch (error) {
+ resolve(false);
+ }
+ });
+ }
+
+ function getNewhttps() {
+ var selectors = [
+ 'input[data-widget-id="widget.cbid.netwizard.default.https"]'
+ ];
+
+ for (var i = 0; i < selectors.length; i++) {
+ var inputs = document.querySelectorAll(selectors[i]);
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (input.type === 'checkbox') {
+ return input.checked ? '1' : '0';
+ } else if (input.type === 'hidden') {
+ return input.value === '1' ? '1' : '0';
+ }
+ }
+ }
+ return '0';
+ }
+
+ function showDHCPWarningMessage() {
+ var overlay = document.createElement('div');
+ overlay.id = 'netwizard-dhcp-warning-overlay';
+ overlay.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.85);
+ z-index: 9999;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: Arial, sans-serif;
+ `;
+
+ var messageBox = document.createElement('div');
+ messageBox.style.cssText = `
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 2%;
+ border-radius: 15px;
+ text-align: center;
+ min-width: 300px;
+ max-width: 600px;
+ box-shadow: rgba(255, 255, 255, 0.2) 0px 20px 40px;
+ `;
+
+ var warningIcon = document.createElement('div');
+ warningIcon.innerHTML = '⚠️';
+ warningIcon.style.cssText = `
+ font-size: 60px;
+ margin-bottom: 10px;
+ animation: pulse 2s infinite;
+ `;
+
+ var style = document.createElement('style');
+ style.textContent = `
+ @keyframes pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
+ }
+ `;
+ document.head.appendChild(style);
+
+ var title = document.createElement('h2');
+ title.textContent = _('Set LAN to DHCP mode');
+ title.style.cssText = `
+ margin: 0 0 20px 0;
+ color: #FFD700;
+ font-size: 28px;
+ `;
+
+ var message = document.createElement('div');
+ message.innerHTML = _('The router is now configured to obtain IP address via DHCP.') +
+ '' +
+ '' + _('Important Note:') + ' ' +
+ '1. ' + _('The current router IP address will be assigned by the DHCP server of the superior router') + ' ' +
+ '2. ' + _('Please login to the superior router to view the DHCP client list') + ' ' +
+ '3. ' + _('Or access using the original IP address on the current router') + ' ' +
+ '4. ' + _('Unable to automatically redirect to the new IP address') +
+ '
' +
+ '' +
+ _('Configuration has been saved successfully. You can manually access the router management interface.') +
+ '
';
+
+ message.style.cssText = `
+ color: rgba(255,255,255,0.9);
+ line-height: 1.5rem;
+ font-size: 0.875rem;
+ `;
+
+ var buttonContainer = document.createElement('div');
+ buttonContainer.style.cssText = `
+ display: flex;
+ justify-content: center;
+ margin-top: 1rem;
+ flex-wrap: wrap;
+ `;
+
+ var closeButton = document.createElement('button');
+ closeButton.textContent = _('Close');
+ closeButton.style.cssText = `
+ background: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 50px;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+ padding: 0 30px;
+ transition: all 0.3s ease;
+ box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
+ `;
+
+ closeButton.onmouseover = function() {
+ this.style.transform = 'translateY(-2px)';
+ this.style.boxShadow = '0 8px 20px rgba(76, 175, 80, 0.6)';
+ };
+
+ closeButton.onmouseout = function() {
+ this.style.transform = 'translateY(0)';
+ this.style.boxShadow = '0 5px 15px rgba(76, 175, 80, 0.4)';
+ };
+
+ closeButton.onclick = function() {
+ document.body.removeChild(overlay);
+ };
+
+ messageBox.appendChild(warningIcon);
+ messageBox.appendChild(title);
+ messageBox.appendChild(message);
+ buttonContainer.appendChild(closeButton);
+ messageBox.appendChild(buttonContainer);
+ overlay.appendChild(messageBox);
+
+ document.body.appendChild(overlay);
+ }
+
+ function showRedirectMessage(newIP, useHTTPS, isDHCP) {
+ if (isDHCP) {
+ showDHCPWarningMessage();
+ return;
+ }
+
+ var overlay = document.createElement('div');
+ overlay.id = 'netwizard-redirect-overlay';
+ overlay.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.85);
+ z-index: 9999;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: Arial, sans-serif;
+ `;
+
+ var messageBox = document.createElement('div');
+ messageBox.style.cssText = `
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 1rem;
+ border-radius: 15px;
+ text-align: center;
+ min-width: 350px;
+ box-shadow: 0 20px 40px rgba(0,0,0,0.3);
+ color: white;
+ `;
+
+ var icon = document.createElement('div');
+ icon.innerHTML = '✓';
+ icon.style.cssText = `
+ font-size: 60px;
+ color: #4CAF50;
+ background: white;
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 20px;
+ font-weight: bold;
+ box-shadow: 0 10px 20px rgba(0,0,0,0.2);
+ `;
+
+ var title = document.createElement('h2');
+ title.textContent = _('Configuration Applied Successfully!');
+ title.style.cssText = `
+ margin: 0 0 20px 0;
+ color: white;
+ `;
+
+ var protocolText = useHTTPS === '1' ? 'HTTPS' : 'HTTP';
+ var message = document.createElement('div');
+
+ message.innerHTML = _('The network configuration has been saved and applied. ') +
+ '' +
+ _('Redirecting to') + ' ' +
+ ''+ newIP + ' ' +
+ _('Access Protocol:') + ' ' +
+ '' +' ' + protocolText + ' ' +
+ '
' +
+ _('The page will automatically redirect in') + ' ' +
+ '10 ' + ' ' +
+ _('seconds...');
+
+ message.style.cssText = `
+ color: rgba(255,255,255,0.9);
+ line-height: 1.8;
+ margin: 20px 0;
+ font-size: 16px;
+ `;
+
+ var buttonContainer = document.createElement('div');
+ buttonContainer.style.cssText = `
+ display: flex;
+ justify-content: center;
+ gap: 15px;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+ `;
+
+ var redirectButton = document.createElement('button');
+ redirectButton.textContent = _('Redirect Now');
+ redirectButton.style.cssText = `
+ background: #4CAF50;
+ color: white;
+ border: none;
+ padding: 0 30px;
+ border-radius: 50px;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
+ `;
+
+ redirectButton.onmouseover = function() {
+ this.style.transform = 'translateY(-2px)';
+ this.style.boxShadow = '0 8px 20px rgba(76, 175, 80, 0.6)';
+ };
+
+ redirectButton.onmouseout = function() {
+ this.style.transform = 'translateY(0)';
+ this.style.boxShadow = '0 5px 15px rgba(76, 175, 80, 0.4)';
+ };
+
+ redirectButton.onclick = function() {
+ redirectToNewIP(newIP, useHTTPS);
+ };
+
+ messageBox.appendChild(icon);
+ messageBox.appendChild(title);
+ messageBox.appendChild(message);
+ buttonContainer.appendChild(redirectButton);
+ messageBox.appendChild(buttonContainer);
+ overlay.appendChild(messageBox);
+
+ document.body.appendChild(overlay);
+
+ var countdown = 10;
+ var countdownElement = document.getElementById('netwizard-countdown');
+
+ var countdownInterval = setInterval(function() {
+ countdown--;
+ if (countdownElement) {
+ countdownElement.textContent = countdown;
+
+ if (countdown <= 3) {
+ countdownElement.style.color = (countdown % 2 === 0) ? '#FF6B6B' : '#FFD700';
+ }
+ }
+
+ if (countdown <= 0) {
+ clearInterval(countdownInterval);
+ redirectToNewIP(newIP, useHTTPS);
+ }
+ }, 1000);
+
+ overlay._countdownInterval = countdownInterval;
+ }
+
+ function hideRedirectMessage() {
+ var overlay = document.getElementById('netwizard-redirect-overlay');
+ if (overlay) {
+ if (overlay._countdownInterval) {
+ clearInterval(overlay._countdownInterval);
+ }
+ document.body.removeChild(overlay);
+ }
+ }
+
+ function redirectToNewIP(newIP, useHTTPS) {
+ hideRedirectMessage();
+
+ var protocol = useHTTPS === '1' ? 'https:' : 'http:';
+ var currentPort = window.location.port ? ':' + window.location.port : '';
+ var newURL = protocol + '//' + newIP + currentPort + '/';
+
+ var jumpMsg = document.createElement('div');
+ jumpMsg.id = 'netwizard-jump-msg';
+ jumpMsg.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #4CAF50;
+ color: white;
+ padding: 15px 25px;
+ border-radius: 10px;
+ z-index: 10000;
+ font-weight: bold;
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
+ animation: slideIn 0.5s ease;
+ `;
+
+ var style = document.createElement('style');
+ style.textContent = `
+ @keyframes slideIn {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+ `;
+ document.head.appendChild(style);
+
+ jumpMsg.textContent = _('Redirecting to') + ' ' + (useHTTPS === '1' ? 'HTTPS://' : 'HTTP://') + newIP + '...';
+ document.body.appendChild(jumpMsg);
+
+ setTimeout(function() {
+ try {
+ window.location.href = newURL;
+ } catch (e) {
+ alert(_('Failed to redirect to') + ' ' + newIP +
+ _('\nPlease manually access:\n') + newURL);
+
+ var jumpMsg = document.getElementById('netwizard-jump-msg');
+ if (jumpMsg) {
+ document.body.removeChild(jumpMsg);
+ }
+ }
+ }, 1000);
+ }
+
+ m.save = function() {
+ var newLanIP = getNewLanIP();
+ var useHTTPS = getNewhttps();
+ var self = this;
+
+ var savingMsg = document.createElement('div');
+ savingMsg.id = 'netwizard-saving-msg';
+ savingMsg.style.cssText = `
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(0,0,0,0.9);
+ color: white;
+ padding: 20px 40px;
+ border-radius: 10px;
+ z-index: 9998;
+ font-size: 16px;
+ `;
+ savingMsg.textContent = _('Saving configuration...');
+ document.body.appendChild(savingMsg);
+
+ var startTime = Date.now();
+
+ function cleanup() {
+ if (savingMsg && savingMsg.parentNode) {
+ document.body.removeChild(savingMsg);
+ }
+ }
+
+ function applywait(wtime) {
+ return new Promise(function(resolve) {
+ setTimeout(resolve, wtime);
+ });
+ }
+
+ return getLanproto()
+ .then(async function(isDHCP) {
+ savingMsg.textContent = _('Saving configuration...');
+ var result = await originalSave.call(m);
+
+ savingMsg.textContent = _('Applying configuration...');
+ await applywait(2000);
+
+ var totalTime = Date.now() - startTime;
+ // console.log('Save time:', totalTime + 'ms');
+ cleanup();
+
+ return {
+ result: result,
+ newLanIP: newLanIP || currentLanIP,
+ isDHCP: isDHCP,
+ useHTTPS: useHTTPS
+ };
+ })
+ .then(function(data) {
+ var result = data.result;
+ var actualNewLanIP = data.newLanIP;
+ var isDHCP = data.isDHCP;
+ var useHTTPS = data.useHTTPS;
+
+ var ipChanged = actualNewLanIP && currentLanIP !== actualNewLanIP;
+ var HTTPSChanged = currentHTTPS !== useHTTPS;
+ var isHTTP = useHTTPS === '0';
+ var needRedirect = true;
+
+ if (isHTTP && !ipChanged && !HTTPSChanged && !isDHCP) {
+ needRedirect = false;
+ }
+
+ if (!needRedirect) {
+ var successMsg = document.createElement('div');
+ successMsg.id = 'netwizard-success-msg';
+ successMsg.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #4CAF50;
+ color: white;
+ padding: 15px 25px;
+ border-radius: 10px;
+ z-index: 9999;
+ font-weight: bold;
+ animation: slideIn 0.5s ease;
+ `;
+ successMsg.textContent = _('Configuration saved successfully!');
+ document.body.appendChild(successMsg);
+
+ setTimeout(function() {
+ if (successMsg && successMsg.parentNode) {
+ document.body.removeChild(successMsg);
+ }
+ }, 3000);
+
+ return result;
+ }
+
+ showRedirectMessage(actualNewLanIP, useHTTPS, isDHCP);
+
+ return result;
+ })
+ .catch(function(err) {
+ var msg = document.getElementById('netwizard-saving-msg');
+ if (msg && msg.parentNode) {
+ document.body.removeChild(msg);
+ }
+
+ var errorMsg = document.createElement('div');
+ errorMsg.id = 'netwizard-error-msg';
+ errorMsg.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #f44336;
+ color: white;
+ padding: 15px 25px;
+ border-radius: 10px;
+ z-index: 9999;
+ font-weight: bold;
+ animation: slideIn 0.5s ease;
+ `;
+ errorMsg.textContent = _('Failed to save configuration');
+ document.body.appendChild(errorMsg);
+
+ setTimeout(function() {
+ var msg = document.getElementById('netwizard-error-msg');
+ if (msg && msg.parentNode) {
+ document.body.removeChild(msg);
+ }
+ }, 5000);
+
+ throw err;
+ });
+ };
+
+ var script = document.createElement('script');
+ script.textContent = `
+ function switchToTab(tabName) {
+ var tabs = document.querySelectorAll('.cbi-tabmenu a');
+ for (var i = 0; i < tabs.length; i++) {
+ var tab = tabs[i];
+ var tabText = tab.textContent || tab.innerText;
+ if ((tabName === 'wansetup' && (tabText.trim() === 'WAN Settings' || tabText.includes('WAN') || tabText.includes('网络设置'))) ||
+ (tabName === 'modesetup' && (tabText.trim() === 'Network Mode' || tabText.includes('Mode') || tabText.includes('网络模式'))) ||
+ (tabName === 'wifisetup' && (tabText.trim() === 'Wireless Settings' || tabText.includes('Wireless') || tabText.includes('无线设置'))) ||
+ (tabName === 'othersetup' && (tabText.trim() === 'Other Settings' || tabText.includes('Other') || tabText.includes('其他设置')))) {
+ tab.click();
+ var tabItems = document.querySelectorAll('.cbi-tabmenu li');
+ tabItems.forEach(function(item) {
+ item.classList.remove('cbi-tab-active');
+ });
+ tab.parentNode.classList.add('cbi-tab-active');
+ break;
+ }
+ }
+ }
+
+ if (window.location.search.includes('selectedMode')) {
+ setTimeout(function() {
+ switchToTab('wansetup');
+ }, 200);
+
+ document.addEventListener('DOMContentLoaded', function() {
+ setTimeout(function() {
+ switchToTab('wansetup');
+ }, 100);
+ });
+ }
+ `;
+
+ document.head.appendChild(script);
+
+ return m.render();
+ }
+});
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/status/include/10_ota.js b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/status/include/10_ota.js
new file mode 100644
index 0000000000..83d819a489
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/htdocs/luci-static/resources/view/status/include/10_ota.js
@@ -0,0 +1,93 @@
+// Copyright 2023-2026 sirpdboy
+'use strict';
+'require baseclass';
+'require rpc';
+
+var callOTACheck = rpc.declare({
+ object: 'ota',
+ method: 'check'
+});
+
+const callUciGet = rpc.declare({
+ object: 'uci',
+ method: 'get',
+ params: ['config', 'section', 'option']
+});
+
+return baseclass.extend({
+ title: _('Firmware Update'),
+
+ load: function() {
+ return Promise.resolve({ code: -1 });
+ },
+
+ render: function() {
+ callUciGet('netwizard', 'default', 'updatacheck')
+ .then((res) => {
+ const updatacheck = res?.value ?? '0';
+ console.log('Update check setting:', updatacheck);
+
+ // 只有当配置为1时才检测更新
+ if (updatacheck == 1 || updatacheck == '1') {
+ setTimeout(() => {
+ this.checkOTAUpdate();
+ }, 1000);
+ }
+ })
+ .catch((err) => {
+ const updatacheck = '0';
+ });
+
+ return null;
+ },
+
+ checkOTAUpdate: function() {
+ if (window.otaCheckStarted) return;
+ window.otaCheckStarted = true;
+
+ callOTACheck()
+ .then(data => {
+ if (data && data.code === 0) {
+ this.addUpdateButton();
+ }
+ })
+ .catch(() => {
+ });
+ },
+
+ addUpdateButton: function() {
+ if (document.getElementById('ota-notice')) {
+ return;
+ }
+
+ var flashindicators = document.querySelector('#indicators');
+ if (!flashindicators) return;
+
+ var notice = document.createElement('div');
+ notice.id = 'ota-notice';
+ notice.innerHTML = [
+ ''
+ ].join('');
+
+ flashindicators.parentNode.insertBefore(notice, flashindicators);
+ this.addResponsiveStyle();
+ },
+
+ addResponsiveStyle: function() {
+ if (document.getElementById('ota-responsive-style')) return;
+
+ var style = document.createElement('style');
+ style.id = 'ota-responsive-style';
+ style.textContent = '@media (max-width: 480px) { header>.fill>.container>.flex1>.brand { display: none; } }';
+ document.head.appendChild(style);
+ }
+});
diff --git a/openwrt-packages/luci-app-netwizard/po/zh-cn b/openwrt-packages/luci-app-netwizard/po/zh-cn
new file mode 120000
index 0000000000..8d69574ddd
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/po/zh-cn
@@ -0,0 +1 @@
+zh_Hans
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/po/zh_Hans/netwizard.po b/openwrt-packages/luci-app-netwizard/po/zh_Hans/netwizard.po
new file mode 100644
index 0000000000..c824461087
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/po/zh_Hans/netwizard.po
@@ -0,0 +1,280 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Netwizard"
+msgstr "设置向导"
+
+msgid "Quick Network Setup Wizard"
+msgstr "快速网络设置向导"
+
+msgid "Quick network setup wizard. If you need more settings, please enter network - interface to set."
+msgstr "快速网络设置向导。如需更多设置,请进入网络-接口 页面进行设置。"
+
+msgid "Network Mode"
+msgstr "网络模式"
+
+msgid "WAN Settings"
+msgstr "网络设置"
+
+msgid "Wireless Settings"
+msgstr "无线设置"
+
+msgid "Set the router's wireless name and password. For more advanced settings, please go to the Network-Wireless page."
+msgstr "设置路由器的无线名称和密码。如需更多高级设置,请前往网络-无线页面。"
+
+msgid "Other Settings"
+msgstr "其他设置"
+
+msgid "Select Network Connection Mode"
+msgstr "选择网络连接模式"
+
+msgid "Choose the connection mode that matches your network environment"
+msgstr "选择与您的网络环境匹配的连接模式"
+
+msgid "PPPoE Dial-up"
+msgstr "宽带拨号连接"
+
+msgid "Fiber broadband that requires username/password"
+msgstr "适用于需要用户名/密码的光纤宽带"
+
+msgid "DHCP Client"
+msgstr "连接现有路由器"
+
+msgid "Connect to router as a subordinate router to internet"
+msgstr "连接到路由器做为下级路由上网"
+
+msgid "Side Router"
+msgstr "配置为旁路由"
+
+msgid "Configure as side router in same network as main router"
+msgstr "配置为与主路由器在同一网络中的旁路路由"
+
+msgid "Network connection mode"
+msgstr "网络连接协议模式"
+
+msgid "Current Network Mode"
+msgstr "当前网络协议模式"
+
+msgid "Go to WAN Settings"
+msgstr "去设置网络参数"
+
+msgid "Change Mode"
+msgstr "更改协议模式"
+
+msgid "Network protocol mode selection"
+msgstr "网络协议模式选择"
+
+msgid "Add LAN port configuration"
+msgstr "增加LAN口参数配置"
+
+msgid "Three different ways to access the Internet, please choose according to your own situation."
+msgstr "三种不同的上网方式,请根据自身情况选择。"
+
+msgid "LAN IP Address Mode"
+msgstr "LAN IP 地址模式"
+
+msgid "Warning: Setting up automatic IP address retrieval requires checking the IP address on the higher-level router"
+msgstr "警告:设置自动获取IP地址须到上级路由上查看IP"
+
+msgid "Static IP address (Specify non conflicting IP addresses)"
+msgstr "静态 IP 地址(指定非冲突的 IP 地址)"
+
+msgid "DHCP client (existing router assigns IP)"
+msgstr "DHCP 客户端(现有路由器分配IP)"
+
+msgid "DHCP client (Main router assigns IP)"
+msgstr "DHCP 客户端(主路由器分配 IP)"
+
+msgid "WAN interface IP address mode"
+msgstr "WAN口IP地址模式"
+
+msgid "Choose how to get IP address for WAN interface"
+msgstr "选择WAN接口获取IP地址的方式"
+
+msgid "LAN IPv4 Address"
+msgstr "LAN口IPv4地址"
+
+msgid "You must specify the IP address of this machine, which is the IP address of the web access route"
+msgstr "您必须指定本机的 IP 地址,即Web访问路由的IP地址"
+
+msgid "LAN IPv4 Netmask"
+msgstr "LAN口IPv4子网掩码"
+
+msgid "LAN IPv4 Gateway"
+msgstr "LAN口IPv4网关"
+
+msgid "Please enter the main routing IP address. The bypass gateway is not the same as the login IP of this bypass WEB and is in the same network segment"
+msgstr "请输入主路由IP地址。旁路网关并非本旁路WEB登录IP,且处于同一网段"
+
+msgid "Use Custom SideRouter DNS"
+msgstr "使用自定义旁路由DNS"
+
+msgid "Ali DNS: 223.5.5.5"
+msgstr "阿里 DNS:223.5.5.5"
+
+msgid "Baidu DNS: 180.76.76.76"
+msgstr "百度 DNS:180.76.76.76"
+
+msgid "114 DNS: 114.114.114.114"
+msgstr "114 DNS:114.114.114.114"
+
+msgid "Google DNS: 8.8.8.8"
+msgstr "谷歌 DNS:8.8.8.8"
+
+msgid "Cloudflare DNS: 1.1.1.1"
+msgstr "Cloudflare DNS:1.1.1.1"
+
+msgid "Device"
+msgstr "设备"
+
+msgid "Allocate the physical interface of WAN port"
+msgstr "分配 WAN 口的物理接口"
+
+msgid "PAP/CHAP Username"
+msgstr "PAP/CHAP 用户名"
+
+msgid "PAP/CHAP Password"
+msgstr "PAP/CHAP 密码"
+
+msgid "WAN IPv4 Address"
+msgstr "WAN IPv4 地址"
+
+msgid "WAN IPv4 Netmask"
+msgstr "WAN IPv4 子网掩码"
+
+msgid "WAN IPv4 Gateway"
+msgstr "WAN IPv4 网关"
+
+msgid "Use Custom WAN DNS"
+msgstr "使用自定义 WAN DNS"
+
+msgid "Enable IPv6"
+msgstr "启用 IPv6"
+
+msgid "Disable DHCP Server"
+msgstr "禁用DHCP服务器"
+
+msgid "Selecting means that the DHCP server is not enabled. In a network, only one DHCP server is needed to allocate and manage client IPs. If it is a siderouter route, it is recommended to turn off the primary routing DHCP server."
+msgstr "选择意味着不启用 DHCP 服务器。在一个网络中,只需要一个 DHCP 服务器来分配和管理客户端 IP。如果是旁路由,建议关闭主路由的 DHCP 服务器。"
+
+msgid "Enable DNS Notifications (IPv4/IPv6)"
+msgstr "启用 DNS 通知(IPv4/IPv6)"
+
+msgid "Forcefully specify the DNS server for this router"
+msgstr "强制指定此路由器的 DNS 服务器"
+
+msgid "DNS"
+msgstr "DNS服务器"
+
+msgid "Use local IP for DNS (default)"
+msgstr "使用本地 IP 作为 DNS(默认)"
+
+msgid "Forcefully Forwarding"
+msgstr "强制转发"
+
+msgid "Forcefully add LAN to WAN forwarding"
+msgstr "强制添加 LAN 到 WAN 的转发"
+
+msgid "Redirect to HTTPS"
+msgstr "重定向到 HTTPS"
+
+msgid "Enable automatic redirection of HTTP requests to HTTPS port."
+msgstr "启用自动将 HTTP 请求重定向到 HTTPS 端口。"
+
+msgid "ESSID "
+msgstr "ESSID "
+
+msgid "Key"
+msgstr "密钥"
+
+msgid "Enable SYN-flood Defense"
+msgstr "启用 SYN 洪水防御"
+
+msgid "Enable Firewall SYN-flood defense [Suggest opening]"
+msgstr "启用防火墙 SYN 洪水防御 [建议开启]"
+
+msgid "Configuration Applied Successfully!"
+msgstr "配置应用成功!"
+
+msgid "The network configuration has been saved and applied. "
+msgstr "网络配置已保存并应用。 "
+
+msgid "The page will automatically redirect in"
+msgstr "页面自动转向还有"
+
+msgid "seconds..."
+msgstr "秒..."
+
+msgid "Redirect Now"
+msgstr "立即转向"
+
+msgid "Redirecting to"
+msgstr "转向地址"
+
+msgid "Failed to redirect to"
+msgstr "无效重定向"
+
+msgid "\nPlease manually access:\n"
+msgstr "\n请手动访问:\n"
+
+msgid "Applying network configuration..."
+msgstr "正在应用网络配置..."
+
+msgid "Saving configuration..."
+msgstr "正在保存配置..."
+
+msgid "Applying configuration..."
+msgstr "正在应用配置..."
+
+msgid "Preparing network settings..."
+msgstr "正在更新网络设置..."
+
+msgid "Configuration saved successfully!"
+msgstr "配置保存成功!"
+
+msgid "Access Protocol:"
+msgstr "访问协议:"
+
+msgid "Failed to save configuration"
+msgstr "保存配置失败"
+
+msgid "Network configuration has been successfully applied!"
+msgstr "网络配置已成功应用!"
+
+msgid "The router is now configured to obtain IP address via DHCP."
+msgstr "路由器现已配置为通过DHCP获取IP地址。"
+
+msgid "Please check your main router DHCP client list or try to access the router using its original IP address."
+msgstr "请检查您主路由器的DHCP客户端列表,或尝试使用其原始IP地址访问路由器。"
+
+msgid "Set LAN to DHCP mode"
+msgstr "LAN设置为DHCP模式"
+
+msgid "Important Note:"
+msgstr "重要提示:"
+
+msgid "The current router IP address will be assigned by the DHCP server of the superior router"
+msgstr "当前路由器IP地址将由上级路由器的DHCP服务器分配"
+
+msgid "Please login to the superior router to view the DHCP client list"
+msgstr "请登录上级路由器查看DHCP客户端列表"
+
+msgid "Or access using the original IP address on the current router"
+msgstr "或在当前路由器使用原始IP地址访问"
+
+msgid "Unable to automatically redirect to the new IP address"
+msgstr "无法自动重定向到新IP地址"
+
+msgid "Configuration has been saved successfully. You can manually access the router management interface."
+msgstr "配置已保存成功,可以手动访问路由器管理界面。"
+
+msgid "Enable detection update prompts"
+msgstr "启用检测更新提示"
+
+msgid "Auto-fetch"
+msgstr "自动获取"
+
+msgid ""
+msgstr ""
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/root/etc/config/netwizard b/openwrt-packages/luci-app-netwizard/root/etc/config/netwizard
new file mode 100644
index 0000000000..68bf57363a
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/etc/config/netwizard
@@ -0,0 +1,2 @@
+config netwizard 'default'
+ option lan_netmask '255.255.255.0'
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/root/etc/init.d/netwizard b/openwrt-packages/luci-app-netwizard/root/etc/init.d/netwizard
new file mode 100644
index 0000000000..5ff4ed7d43
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/etc/init.d/netwizard
@@ -0,0 +1,720 @@
+#!/bin/sh /etc/rc.common
+
+START=99
+
+UCISET='uci -q set'
+UCIDEL='uci -q delete'
+UCIADD='uci -q add'
+UCIADDLIST='uci -q add_list'
+LOCK=/var/lock/netwizard-boot.lock
+LOCK_TIMEOUT=300
+
+log() {
+ echo "netwizard:$1"
+}
+
+validate_ip() {
+ echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$' && return 0 || return 1
+}
+
+validate_cidr() {
+ echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' && return 0 || return 1
+}
+
+validate_interface() {
+ [ -n "$1" ] && [ -d "/sys/class/net/$1" ] && return 0 || return 1
+}
+
+# 检查锁文件
+check_lock() {
+ if [ -f "$LOCK" ]; then
+ local lock_age=$(($(date +%s) - $(stat -c %Y "$LOCK" 2>/dev/null || echo 0)))
+ if [ $lock_age -gt $LOCK_TIMEOUT ]; then
+ log "Removing stale lock file (age: ${lock_age}s)"
+ rm -f "$LOCK"
+ return 1
+ fi
+ log "Another instance is running, exiting"
+ return 0
+ fi
+ return 1
+}
+
+create_lock() {
+ touch "$LOCK" 2>/dev/null || {
+ log_error "Failed to create lock file"
+ return 1
+ }
+ return 0
+}
+
+remove_lock() {
+ rm -f "$LOCK" 2>/dev/null
+}
+
+check_dependencies() {
+ local missing=""
+
+ for cmd in $REQUIRED_COMMANDS; do
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ missing="$missing $cmd"
+ fi
+ done
+
+ if [ -n "$missing" ]; then
+ log_error "Required commands not found:$missing"
+ return 1
+ fi
+
+ return 0
+}
+setipv6() {
+ case $1 in
+ 0)
+ bINDEX=$(uci show network | grep -E "@device\[[0-9]+\].name='br-lan'" | sed -n 's/.*@device\[\([0-9]*\)\].name.*/\1/p')
+ if [ -n "$bINDEX" ]; then
+ $UCISET network.@device[$bINDEX].ipv6='0'
+ fi
+ nINDEX=$(uci show network | grep -E "@device\[[0-9]+\].name=\'$wan_interface\'" | sed -n 's/.*@device\[\([0-9]*\)\].name.*/\1/p')
+ [ -n "$nINDEX" ] && $UCIDEL network.@device[$nINDEX]
+ uci -q batch </dev/null 2>&1; do
+ local src=$(uci -q get firewall.@forwarding[$idx].src)
+ local dest=$(uci -q get firewall.@forwarding[$idx].dest)
+ if [ "$src" = "lan" ] && [ "$dest" = "wan" ]; then
+ $UCIDEL firewall.@forwarding[$idx]
+ break
+ fi
+ idx=$((idx + 1))
+ done
+ $UCIADD firewall forwarding
+ $UCISET firewall.@forwarding[-1].src='lan'
+ $UCISET firewall.@forwarding[-1].dest='wan'
+}
+
+# 设置HTTPS
+sethttps() {
+ local cfg=$1
+ local https
+
+ config_get https "$cfg" https '0'
+
+ if [ -n "$(command -v nginx)" ]; then
+ if [ -z "$(uci -q get nginx._redirect2ssl)" ]; then
+ $UCISET nginx._redirect2ssl=server
+ $UCISET nginx._redirect2ssl.server_name='_redirect2ssl'
+ $UCISET nginx._redirect2ssl.return='302 https://$host$request_uri'
+ $UCISET nginx._redirect2ssl.access_log='off; # logd openwrt'
+ fi
+
+ if [ "x${https}" = "x1" ]; then
+ $UCIDEL nginx.default_server.listen
+ $UCIADDLIST nginx.default_server.listen='80'
+ $UCIADDLIST nginx.default_server.listen='[::]:80'
+ $UCIDEL nginx._redirect2ssl.listen
+ $UCIADDLIST nginx._redirect2ssl.listen='80 default_server'
+ $UCIADDLIST nginx._redirect2ssl.listen='[::]:80 default_server'
+ else
+ $UCIDEL nginx._redirect2ssl.listen
+ $UCIADDLIST nginx._redirect2ssl.listen='80'
+ $UCIADDLIST nginx._redirect2ssl.listen='[::]:80'
+ $UCIDEL nginx.default_server.listen
+ $UCIADDLIST nginx.default_server.listen='80 default_server'
+ $UCIADDLIST nginx.default_server.listen='[::]:80 default_server'
+ fi
+ uci commit nginx
+ /etc/init.d/nginx reload
+ else
+ /etc/init.d/uhttpd stop 2>/dev/null
+ sed -i "/listen_https/d" /etc/config/uhttpd 2>/dev/null
+ $UCISET uhttpd.main.redirect_https='0'
+
+ if [ "x${https}" = "x1" ]; then
+ certscrt='/etc/ssl/ezopwrt.crt'
+ certskey='/etc/ssl/ezopwrt.key'
+
+ # 确保证书目录存在
+ mkdir -p /etc/ssl/
+ chmod 755 /etc/ssl/
+
+ rm -f $certskey $certscrt 2>/dev/null
+ $UCIADDLIST uhttpd.main.listen_https='0.0.0.0:443'
+ $UCIADDLIST uhttpd.main.listen_https='[::]:443'
+ $UCISET uhttpd.main.redirect_https='1'
+ hostname=$(uci -q get system.@system[0].hostname | awk '{print tolower($0)}' || echo 'openwrt')
+ $UCISET network.lan.hostname=${hostname}
+ $UCISET dhcp.@dnsmasq[0].domain="${hostname}.lan"
+
+ # 生成证书
+ openssl req -new -newkey rsa:2048 -days 3650 -sha256 -nodes -x509 \
+ -keyout $certskey \
+ -out $certscrt \
+ -subj "/C=CN/CN=$hostname.lan" 2>/dev/null
+
+ if [ $? -eq 0 ] && [ -f "$certskey" ] && [ -f "$certscrt" ]; then
+ chmod 600 $certskey
+ chmod 644 $certscrt
+ $UCISET uhttpd.main.cert="$certscrt"
+ $UCISET uhttpd.main.key="$certskey"
+ else
+ log "Failed to generate SSL certificate"
+ $UCIDEL uhttpd.main.cert
+ $UCIDEL uhttpd.main.key
+ fi
+ fi
+ fi
+}
+
+# 备份配置
+backup_configs() {
+ local backup_dir="/tmp/netwizard_backup_$(date +%s)"
+ mkdir -p "$backup_dir"
+ for config in network firewall dhcp wireless system uhttpd nginx; do
+ if [ -f "/etc/config/$config" ]; then
+ cp "/etc/config/$config" "$backup_dir/" 2>/dev/null
+ fi
+ done
+ echo "$backup_dir"
+}
+
+# 恢复配置
+restore_configs() {
+ local backup_dir="$1"
+ if [ -d "$backup_dir" ]; then
+ log "Restoring configurations from backup"
+ for config in "$backup_dir"/*; do
+ if [ -f "$config" ]; then
+ cp "$config" "/etc/config/" 2>/dev/null
+ fi
+ done
+ rm -rf "$backup_dir"
+ fi
+}
+
+configure_network() {
+ local cfg=$1
+ local wan_proto wan_ipaddr wan_netmask wan_gateway wan_dns wan_pppoe_user wan_pppoe_pass
+ local ipv6 wifi_ssid wifi_key old_wifi_ssid old_wifi_key showhide
+ local lan_ipaddr lan_netmask lan_gateway lan_dns lan_dhcp wan_interface lan_proto
+ local dns_tables synflood https
+ local ifname lannet netname netsum i
+
+ # 获取配置
+ config_get showhide "$cfg" showhide
+ config_get wan_proto "$cfg" wan_proto
+ config_get ipv6 "$cfg" ipv6 '0'
+ config_get dnsset "$cfg" dnsset '0'
+ config_get wan_interface "$cfg" wan_interface
+ config_get lan_dhcp "$cfg" lan_dhcp '0'
+ config_get synflood "$cfg" synflood '0'
+ config_get lan_ipaddr "$cfg" lan_ipaddr
+ config_get lan_netmask "$cfg" lan_netmask '255.255.255.0'
+ config_get lan_proto "$cfg" lan_proto 'static'
+ config_get lan_gateway "$cfg" lan_gateway
+ config_get lan_dns "$cfg" lan_dns
+ config_get dns_tables "$cfg" dns_tables
+ config_get https "$cfg" https '0'
+ config_get wan_ipaddr "$cfg" wan_ipaddr
+ config_get wan_netmask "$cfg" wan_netmask
+ config_get wan_gateway "$cfg" wan_gateway
+ config_get wan_dns "$cfg" wan_dns
+ config_get wan_pppoe_user "$cfg" wan_pppoe_user
+ config_get wan_pppoe_pass "$cfg" wan_pppoe_pass
+ config_get dhcp_proto "$cfg" dhcp_proto 'dhcp'
+ config_get wifi_ssid "$cfg" wifi_ssid
+ config_get wifi_key "$cfg" wifi_key
+ config_get old_wifi_ssid "$cfg" old_wifi_ssid
+ config_get old_wifi_key "$cfg" old_wifi_key
+
+ # 备份当前配置
+ local backup_dir=$(backup_configs)
+
+ if [ "x$showhide" = "x1" ]; then
+ touch /etc/netwizard_hide 2>/dev/null
+ $UCISET advancedplus.@basic[0].wizard="1"
+ exit 0
+ else
+ rm -rf /etc/netwizard_hide 2>/dev/null
+ $UCISET advancedplus.@basic[0].wizard="0"
+ fi
+
+ if [ -z "$wan_proto" ]; then
+ wan_proto=$(uci -q get network.wan.proto)
+ [ -z "$wan_proto" ] && wan_proto="siderouter"
+ fi
+
+
+ ifname=$(uci -q get network.lan.ifname)
+ [ "x$ifname" = "x" ] && ifname="device" || ifname="ifname"
+
+ [ -n "$wan_interface" ] || wan_interface=$(uci -q get network.wan.$ifname)
+
+ $UCIDEL network.lan.gateway
+ $UCIDEL network.lan.dns
+ $UCIDEL firewall.@zone[0].masq
+
+
+ [ -n "$(uci -q get network.wan)" ] && {
+ $UCIDEL network.wan
+ WAN_INDEX=$(uci show firewall | grep -E "@zone\[[0-9]+\].name='wan'" | sed -n 's/.*@zone\[\([0-9]*\)\].name.*/\1/p')
+ [ -n "$WAN_INDEX" ] && $UCIDEL firewall.@zone[$WAN_INDEX]
+ }
+ [ -n "$(uci -q get network.wan6)" ] && $UCIDEL network.wan6
+ [ -n "$(uci -q get network.lan6)" ] && $UCIDEL network.lan6
+
+ netname=$(ls /sys/class/net/ 2>/dev/null | grep -E '^(eth[0-9]+|en[op][0-9]+s[0-9]+|usb[0-9]+|wlan[0-9]+|wl[0-9]+)' | sort)
+ netsum=$(echo "$netname" | wc -l 2>/dev/null || echo 0)
+
+ if [ "$netsum" -eq 0 ]; then
+ restore_configs "$backup_dir"
+ log "No network interfaces found"
+ return 1
+ elif [ "$netsum" -eq 1 ]; then
+ lannet=$(echo "$netname")
+ wan_interface=$(echo "$netname")
+ elif [ "$netsum" -gt 1 ]; then
+ [ -z "$wan_interface" ] && wan_interface=$(echo "$netname" | grep '^eth' | tail -n 1)
+ [ -z "$wan_interface" ] && wan_interface=$(echo "$netname" | grep -v '^wl' | grep -v '^wlan' | head -n 1)
+ [ -z "$wan_interface" ] && wan_interface=$(echo "$netname" | grep -E '^en[opx][0-9]+' | tail -n 1)
+ lannet=""
+ for eth_interface in $netname; do
+ if [ "$eth_interface" != "$wan_interface" ]; then
+ lannet=$lannet" "${eth_interface##*/}
+ fi
+ done
+ lannet=$(echo "$lannet" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
+ fi
+
+ if [ -n "$wan_interface" ] && [ "$wan_proto" != "siderouter" ]; then
+ $UCISET network.wan=interface
+ $UCISET network.wan6=interface
+ $UCISET network.wan6.proto='dhcpv6'
+ $UCISET network.wan6.delegate='1'
+ $UCIDEL dhcp.wan.master
+ $UCIDEL dhcp.lan.master
+ $UCISET network.wan.metric='1'
+ $UCISET network.wan.$ifname="${wan_interface}"
+ [ "$wan_proto" == "pppoe" ] && $UCISET network.wan6.$ifname="@wan" || $UCISET network.wan6.$ifname="${wan_interface}"
+ # firewall ipv6
+ $UCIDEL dhcp.wan.ra_flags
+
+ $UCIADD firewall zone
+ $UCISET firewall.@zone[-1].name='wan'
+ $UCISET firewall.@zone[-1].input='REJECT'
+ $UCISET firewall.@zone[-1].output='ACCEPT'
+ $UCISET firewall.@zone[-1].forward='REJECT'
+ $UCIADDLIST firewall.@zone[-1].network='wan6'
+ $UCIADDLIST firewall.@zone[-1].network='wan'
+ $UCISET firewall.@zone[-1].masq='1'
+ $UCISET firewall.@zone[-1].masq6='1'
+ $UCISET firewall.@zone[0].masq6='0'
+ fi
+
+ $UCISET firewall.@defaults[0].drop_invalid='1'
+
+ case "${wan_proto}" in
+ pppoe)
+ $UCISET network.wan.proto='pppoe'
+ [ -n "${wan_pppoe_user}" ] && $UCISET network.wan.username="${wan_pppoe_user}"
+ [ -n "${wan_pppoe_pass}" ] && $UCISET network.wan.password="${wan_pppoe_pass}"
+ setforwarding
+ [ "x$ipv6" = "x1" ] && setipv6 1 || setipv6 0
+
+ if [ -n "${wan_dns}" ]; then
+ $UCISET network.wan.peerdns='0'
+ for dns in ${wan_dns}; do
+ $UCIADDLIST network.wan.dns="$dns"
+ done
+ else
+ $UCIDEL network.wan.peerdns
+ $UCIDEL network.wan.dns
+ fi
+ ;;
+
+ dhcp)
+ case "${dhcp_proto}" in
+ static)
+ $UCISET network.wan.proto='static'
+ if [ -n "$wan_ipaddr" ]; then
+ $UCISET network.wan.ipaddr="${wan_ipaddr}"
+ fi
+ if [ -n "$wan_netmask" ]; then
+ $UCISET network.wan.netmask="${wan_netmask}"
+ fi
+ if [ -n "$wan_gateway" ]; then
+ $UCISET network.wan.gateway="${wan_gateway}"
+ fi
+ if [ -n "${wan_dns}" ]; then
+ $UCISET network.wan.defaultroute='1'
+ for dns in ${wan_dns}; do
+ $UCIADD network.wan.dns="$dns"
+ done
+ else
+ $UCIDEL network.wan.dns
+ fi
+ ;;
+
+ dhcp)
+ $UCISET network.wan.proto='dhcp'
+ $UCISET network.wan.delegate='0'
+ if [ -n "${wan_dns}" ]; then
+ $UCISET network.wan.peerdns='0'
+ for dns in ${wan_dns}; do
+ $UCIADDLIST network.wan.dns="$dns"
+ done
+ else
+ $UCIDEL network.wan.peerdns
+ $UCIDEL network.wan.dns
+ fi
+ ;;
+ esac
+ setforwarding
+ [ "x$ipv6" = "x1" ] && setipv6 3 || setipv6 0
+ ;;
+
+ siderouter)
+ $UCIDEL firewall.@zone[0].network
+ $UCIADDLIST firewall.@zone[0].network='lan'
+ $UCIDEL dhcp.lan.ra_slaac
+
+ case "${lan_proto}" in
+ static)
+ $UCISET network.lan.proto='static'
+ if [ -n "$lan_ipaddr" ]; then
+ $UCISET network.lan.ipaddr="${lan_ipaddr}"
+ fi
+ if [ -n "$lan_netmask" ]; then
+ $UCISET network.lan.netmask="${lan_netmask}"
+ fi
+ if [ -n "$lan_gateway" ]; then
+ $UCISET network.lan.gateway="${lan_gateway}"
+ fi
+ if [ -n "$lan_dns" ]; then
+ for dns in ${lan_dns}; do
+ $UCIADDLIST network.lan.dns="$dns"
+ done
+ else
+ $UCIDEL network.lan.dns
+ fi
+ ;;
+
+ dhcp)
+ $UCISET network.lan.proto='dhcp'
+ $UCIDEL network.lan.ipaddr
+ $UCIDEL network.lan.netmask
+ if [ -n "$lan_dns" ]; then
+ $UCISET network.lan.peerdns='0'
+ for dns in ${lan_dns}; do
+ $UCIADDLIST network.lan.dns="$dns"
+ done
+ else
+ $UCIDEL network.lan.peerdns
+ $UCIDEL network.lan.dns
+ fi
+ ;;
+ esac
+
+ if [ $netsum -gt 1 ]; then
+ [ -n "$wan_interface" ] && lannet=$lannet" "${wan_interface##*/}
+ fi
+
+ $UCISET firewall.@zone[0].masq='1'
+ $UCIDEL firewall.@zone[0].network
+ $UCIADDLIST firewall.@zone[0].network='lan'
+
+ [ $(uci show network 2>/dev/null | grep utun | wc -l) -gt 1 ] && $UCIADDLIST firewall.@zone[0].network='utun'
+
+ if [ "x$ipv6" = "x1" ]; then
+ $UCISET network.lan6=interface
+ $UCISET network.lan6.proto='dhcpv6'
+ $UCISET network.lan6.delegate='1'
+ $UCISET network.lan6.$ifname="@lan"
+ setipv6 2
+ $UCIADDLIST firewall.@zone[0].network='lan6'
+ else
+ setipv6 0
+ fi
+ ;;
+ esac
+
+ [ "x$ifname" = "xdevice" ] && $UCISET network.@$ifname[0].ports="${lannet}" || $UCISET network.lan.$ifname="${lannet}"
+ if [ -f /etc/config/dockerd ]; then
+ DOCKERINDEX=$(uci show firewall | grep -E "@zone\[[0-9]+\].name='docker'" | sed -n 's/.*@zone\[\([0-9]*\)\].name.*/\1/p')
+ [ -n "$DOCKERINDEX" ] && $UCIDEL firewall.@zone[$DOCKERINDEX]
+ uci -q batch </dev/null
+ sed -i "/list dns/d" /etc/config/dhcp 2>/dev/null
+ if [ "x$lan_dhcp" = "x1" ]; then
+ $UCIDEL dhcp.lan.force
+ $UCISET dhcp.lan.ignore='1'
+ $UCISET dhcp.lan.dynamicdhcp='0'
+ $UCISET dhcp.lan.ra_slaac="1"
+ $UCIDEL dhcp.@dnsmasq[0].authoritative
+ else
+ $UCIDEL dhcp.lan.ignore
+ $UCIDEL dhcp.lan.dynamicdhcp
+ $UCISET dhcp.lan.force='1'
+ $UCISET dhcp.@dnsmasq[0].authoritative='1'
+
+ if [ "x$dnsset" = "x1" ]; then
+ if [ "${dns_tables}" = "1" ]; then
+ if [ -n "$lan_ipaddr" ]; then
+ $UCIADDLIST dhcp.lan.dhcp_option="6,${lan_ipaddr}"
+ fi
+ elif [ -n "${dns_tables}" ]; then
+ $UCIADDLIST dhcp.lan.dhcp_option="6,${dns_tables}"
+ fi
+ fi
+ fi
+
+ if [ -n "${wifi_ssid}" ] && [ -n "${wifi_key}" ]; then
+ idx=0
+ max_wifi_ifaces=10
+ while [ $idx -lt $max_wifi_ifaces ] && uci -q get wireless.@wifi-iface[$idx] >/dev/null; do
+ if [ "$(uci -q get wireless.@wifi-iface[$idx].mode)" = "ap" ]; then
+ $UCISET wireless.@wifi-iface[$idx].ssid="${wifi_ssid}"
+ $UCISET wireless.@wifi-iface[$idx].key="${wifi_key}"
+ $UCISET wireless.@wifi-iface[$idx].encryption='psk2'
+ fi
+ idx=$((idx + 1))
+ done
+
+ for radio in radio0 radio1 radio2 radio3; do
+ if uci -q get wireless.${radio} >/dev/null 2>&1; then
+ if [ "$(uci -q get wireless.${radio}.band)" = "5g" ]; then
+ $UCISET wireless.default_${radio}.ssid="${wifi_ssid}_5G"
+ else
+ $UCISET wireless.default_${radio}.ssid="${wifi_ssid}_2.4G"
+ fi
+ $UCISET wireless.default_${radio}.device="${radio}"
+ $UCISET wireless.default_${radio}.encryption='psk2'
+ $UCISET wireless.default_${radio}.key="${wifi_key}"
+ fi
+ done
+
+ $UCISET netwizard.default.old_wifi_ssid="${wifi_ssid}"
+ $UCISET netwizard.default.old_wifi_key="${wifi_key}"
+ uci commit wireless
+ fi
+
+ # 提交所有配置
+ uci -q batch <<'COMMIT_EOF' >/dev/null
+ commit advancedplus
+ commit uhttpd
+ commit system
+ commit netwizard
+ commit dhcp
+ commit firewall
+ commit network
+COMMIT_EOF
+
+ if [ $? -eq 0 ]; then
+ {
+
+ /etc/init.d/network restart 2>/dev/null
+ /etc/init.d/system reload 2>/dev/null
+ /etc/init.d/rpcd reload 2>/dev/null
+ /etc/init.d/uhttpd reload 2>/dev/null
+ sleep 1
+ /etc/init.d/dnsmasq reload 2>/dev/null
+ /etc/init.d/firewall reload 2>/dev/null
+ } >/dev/null 2>&1 &
+ rm -rf "$backup_dir" 2>/dev/null
+ log "Configuration applied successfully"
+ else
+ log "Failed to commit configuration, restoring backup"
+ restore_configs "$backup_dir"
+ return 1
+ fi
+}
+
+boot() {
+ XBOOT=1
+ start
+}
+
+start() {
+ check_lock && exit 0
+ # 启动时跳过
+ [ "x$XBOOT" = "x1" ] && exit 0
+
+ # 检查依赖
+ for dep in uci awk grep sed; do
+ if ! command -v $dep >/dev/null 2>&1; then
+ log "ERROR: Required command '$dep' not found"
+ return 1
+ fi
+ done
+
+ create_lock || return 1
+ config_load netwizard
+ config_foreach configure_network netwizard
+ remove_lock
+}
+
+stop() {
+ remove_lock
+}
+
+restart() {
+ stop
+ sleep 1
+ start
+}
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/root/etc/uci-defaults/40-luci-netwizard b/openwrt-packages/luci-app-netwizard/root/etc/uci-defaults/40-luci-netwizard
new file mode 100644
index 0000000000..54a6ffc77b
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/etc/uci-defaults/40-luci-netwizard
@@ -0,0 +1,21 @@
+#!/bin/sh
+[ ! -f "/usr/share/ucitrack/luci-app-netwizard.json" ] && {
+ cat > /usr/share/ucitrack/luci-app-netwizard.json << EEOF
+{
+ "config": "netwizard",
+ "init": "netwizard"
+}
+EEOF
+}
+
+uci -q batch <<-EOF >/dev/null
+ delete ucitrack.@netwizard[-1]
+ add ucitrack netwizard
+ set ucitrack.@netwizard[-1].init=netwizard
+ commit ucitrack
+EOF
+
+chmod +x /etc/init.d/netwizard 2>/dev/null
+rm -f /tmp/luci-indexcache
+
+exit 0
diff --git a/openwrt-packages/luci-app-netwizard/root/usr/libexec/rpcd/luci.netwizard b/openwrt-packages/luci-app-netwizard/root/usr/libexec/rpcd/luci.netwizard
new file mode 100644
index 0000000000..e92d397e2b
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/usr/libexec/rpcd/luci.netwizard
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+# author github@sirpdboy
+
+# Copyright (C) 2019-2026 The Sirpdboy Team
+. /usr/share/libubox/jshn.sh
+
+get_lan_ip() {
+ # 方法1:使用ip命令
+ lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep inet | awk '{print $2}' | cut -d/ -f1)
+
+ # 如果为空,尝试ubus方法
+ if [ -z "$lan_ip" ]; then
+ lan_ip=$(ubus call network.interface.lan status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
+ fi
+
+ echo "$lan_ip"
+}
+
+# 网络向导RPC服务
+case "$1" in
+"list")
+
+ json_init
+ json_add_object "get_ip"
+ json_close_object
+ json_dump
+ json_cleanup
+ ;;
+
+"call")
+ case "$2" in
+ "get_ip")
+
+ read -r input
+
+ lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep inet | awk '{print $2}' | cut -d/ -f1)
+ if [ -z "$lan_ip" ]; then
+ lan_ip=$(ubus call network.interface.lan status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
+ fi
+
+
+ json_init
+ json_add_string "ip" "$lan_ip"
+ json_add_boolean "success" 1
+ json_dump
+ json_cleanup
+ ;;
+ *)
+ echo '{"error": "Method not found"}'
+ ;;
+ esac
+ ;;
+
+ *)
+ echo '{"error": "Invalid action"}'
+ ;;
+esac
\ No newline at end of file
diff --git a/openwrt-packages/luci-app-netwizard/root/usr/share/luci/menu.d/luci-app-netwizard.json b/openwrt-packages/luci-app-netwizard/root/usr/share/luci/menu.d/luci-app-netwizard.json
new file mode 100644
index 0000000000..f39a7234da
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/usr/share/luci/menu.d/luci-app-netwizard.json
@@ -0,0 +1,13 @@
+{
+ "admin/netwizard": {
+ "title": "Netwizard",
+ "order": 11,
+ "action": {
+ "type": "view",
+ "path": "netwizard/netwizard"
+ },
+ "depends": {
+ "acl": [ "luci-app-netwizard" ]
+ }
+ }
+}
diff --git a/openwrt-packages/luci-app-netwizard/root/usr/share/rpcd/acl.d/luci-app-netwizard.json b/openwrt-packages/luci-app-netwizard/root/usr/share/rpcd/acl.d/luci-app-netwizard.json
new file mode 100644
index 0000000000..bc97c544f9
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/usr/share/rpcd/acl.d/luci-app-netwizard.json
@@ -0,0 +1,19 @@
+{
+ "luci-app-netwizard": {
+ "description": "Grant UCI access for luci-app-netwizard",
+ "read": {
+ "ubus": {
+ "luci.netwizard": ["*"],
+ "file": ["exec"]
+ },
+ "uci": [ "netwizard", "wireless" ]
+ },
+ "write": {
+ "ubus": {
+ "luci.netwizard": ["*"],
+ "file": ["exec"]
+ },
+ "uci": [ "netwizard" ]
+ }
+ }
+}
diff --git a/openwrt-packages/luci-app-netwizard/root/usr/share/ucitrack/luci-app-netwizard.json b/openwrt-packages/luci-app-netwizard/root/usr/share/ucitrack/luci-app-netwizard.json
new file mode 100644
index 0000000000..4749abc42d
--- /dev/null
+++ b/openwrt-packages/luci-app-netwizard/root/usr/share/ucitrack/luci-app-netwizard.json
@@ -0,0 +1,4 @@
+{
+ "config": "netwizard",
+ "init": "netwizard"
+}
diff --git a/sing-box/common/tls/apple_client_platform.go b/sing-box/common/tls/apple_client_platform.go
index 38cef976f3..9e7d6e73a2 100644
--- a/sing-box/common/tls/apple_client_platform.go
+++ b/sing-box/common/tls/apple_client_platform.go
@@ -102,7 +102,20 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
return nil, err
}
- connectionState, rawCerts, err := copyAppleTLSConnectionState(client)
+ var state C.box_apple_tls_state_t
+ stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr)
+ if !bool(stateOK) {
+ C.box_apple_tls_client_cancel(client)
+ C.box_apple_tls_client_free(client)
+ if errorPtr != nil {
+ defer C.free(unsafe.Pointer(errorPtr))
+ return nil, E.New(C.GoString(errorPtr))
+ }
+ return nil, E.New("apple TLS: read metadata")
+ }
+ defer C.box_apple_tls_state_free(&state)
+
+ connectionState, rawCerts, err := parseAppleTLSState(&state)
if err != nil {
C.box_apple_tls_client_cancel(client)
C.box_apple_tls_client_free(client)
@@ -127,23 +140,6 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
const appleTLSHandshakePollInterval = 100 * time.Millisecond
-func copyAppleTLSConnectionState(client *C.box_apple_tls_client_t) (tls.ConnectionState, [][]byte, error) {
- var (
- state C.box_apple_tls_state_t
- errorPtr *C.char
- )
- stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr)
- if !bool(stateOK) {
- if errorPtr != nil {
- defer C.free(unsafe.Pointer(errorPtr))
- return tls.ConnectionState{}, nil, E.New(C.GoString(errorPtr))
- }
- return tls.ConnectionState{}, nil, E.New("apple TLS: read metadata")
- }
- defer C.box_apple_tls_state_free(&state)
- return parseAppleTLSState(&state)
-}
-
func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error {
for {
if err := ctx.Err(); err != nil {
diff --git a/sing-box/common/tls/apple_client_platform_darwin.m b/sing-box/common/tls/apple_client_platform_darwin.m
index 7ce49158c4..d03f9fff93 100644
--- a/sing-box/common/tls/apple_client_platform_darwin.m
+++ b/sing-box/common/tls/apple_client_platform_darwin.m
@@ -113,83 +113,44 @@ static void box_set_error_from_nw_error(char **error_out, nw_error_t error) {
CFRelease(cfError);
}
-static bool box_apple_tls_metadata_load_owned_string(
- sec_protocol_metadata_t metadata,
- const char *copy_symbol,
- const char *get_symbol,
- box_sec_protocol_metadata_string_accessor_f *copy_fn,
- box_sec_protocol_metadata_string_accessor_f *get_fn,
- dispatch_once_t *once_token,
- char **value_out,
- char **error_out
-) {
- dispatch_once(once_token, ^{
- *copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, copy_symbol);
- *get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, get_symbol);
+static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) {
+ static box_sec_protocol_metadata_string_accessor_f copy_fn;
+ static box_sec_protocol_metadata_string_accessor_f get_fn;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_negotiated_protocol");
+ get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_negotiated_protocol");
});
- if (*copy_fn != NULL) {
- const char *value = (*copy_fn)(metadata);
- if (value == NULL) {
- *value_out = NULL;
- return true;
- }
- char *owned = strdup(value);
- free((void *)value);
- if (owned == NULL) {
- box_set_error_message(error_out, "apple TLS: out of memory");
- return false;
- }
- *value_out = owned;
- return true;
+ if (copy_fn != NULL) {
+ return (char *)copy_fn(metadata);
}
- if (*get_fn != NULL) {
- const char *value = (*get_fn)(metadata);
- if (value == NULL) {
- *value_out = NULL;
- return true;
+ if (get_fn != NULL) {
+ const char *protocol = get_fn(metadata);
+ if (protocol != NULL) {
+ return strdup(protocol);
}
- char *owned = strdup(value);
- if (owned == NULL) {
- box_set_error_message(error_out, "apple TLS: out of memory");
- return false;
- }
- *value_out = owned;
- return true;
}
- *value_out = NULL;
- return true;
+ return NULL;
}
-static bool box_apple_tls_metadata_load_negotiated_protocol(sec_protocol_metadata_t metadata, char **value_out, char **error_out) {
+static char *box_apple_tls_metadata_copy_server_name(sec_protocol_metadata_t metadata) {
static box_sec_protocol_metadata_string_accessor_f copy_fn;
static box_sec_protocol_metadata_string_accessor_f get_fn;
static dispatch_once_t onceToken;
- return box_apple_tls_metadata_load_owned_string(
- metadata,
- "sec_protocol_metadata_copy_negotiated_protocol",
- "sec_protocol_metadata_get_negotiated_protocol",
- ©_fn,
- &get_fn,
- &onceToken,
- value_out,
- error_out
- );
-}
-
-static bool box_apple_tls_metadata_load_server_name(sec_protocol_metadata_t metadata, char **value_out, char **error_out) {
- static box_sec_protocol_metadata_string_accessor_f copy_fn;
- static box_sec_protocol_metadata_string_accessor_f get_fn;
- static dispatch_once_t onceToken;
- return box_apple_tls_metadata_load_owned_string(
- metadata,
- "sec_protocol_metadata_copy_server_name",
- "sec_protocol_metadata_get_server_name",
- ©_fn,
- &get_fn,
- &onceToken,
- value_out,
- error_out
- );
+ dispatch_once(&onceToken, ^{
+ copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_server_name");
+ get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_server_name");
+ });
+ if (copy_fn != NULL) {
+ return (char *)copy_fn(metadata);
+ }
+ if (get_fn != NULL) {
+ const char *server_name = get_fn(metadata);
+ if (server_name != NULL) {
+ return strdup(server_name);
+ }
+ }
+ return NULL;
}
static NSArray *box_split_lines(const char *content, size_t content_len) {
@@ -293,64 +254,6 @@ static nw_connection_t box_apple_tls_create_connection(int connected_socket, nw_
return create_fn(connected_socket, parameters);
}
-static bool box_apple_tls_state_load_sec_metadata(sec_protocol_metadata_t sec_metadata, box_apple_tls_state_t *state, char **error_out) {
- box_apple_tls_state_reset(state);
- if (sec_metadata == NULL) {
- box_set_error_message(error_out, "apple TLS: metadata unavailable");
- return false;
- }
- state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata);
- state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata);
- if (!box_apple_tls_metadata_load_negotiated_protocol(sec_metadata, &state->alpn, error_out)) {
- box_apple_tls_state_reset(state);
- return false;
- }
- if (!box_apple_tls_metadata_load_server_name(sec_metadata, &state->server_name, error_out)) {
- box_apple_tls_state_reset(state);
- return false;
- }
-
- NSMutableData *chain_data = [NSMutableData data];
- sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) {
- SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate);
- if (certificate_ref == NULL) {
- return;
- }
- CFDataRef certificate_data = SecCertificateCopyData(certificate_ref);
- CFRelease(certificate_ref);
- if (certificate_data == NULL) {
- return;
- }
- uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data);
- uint32_t network_len = htonl(certificate_len);
- [chain_data appendBytes:&network_len length:sizeof(network_len)];
- [chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len];
- CFRelease(certificate_data);
- });
- if (chain_data.length > 0) {
- state->peer_cert_chain = malloc(chain_data.length);
- if (state->peer_cert_chain == NULL) {
- box_set_error_message(error_out, "apple TLS: out of memory");
- box_apple_tls_state_reset(state);
- return false;
- }
- memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length);
- state->peer_cert_chain_len = chain_data.length;
- }
- return true;
-}
-
-static bool box_apple_tls_client_capture_state(box_apple_tls_client_t *client, sec_protocol_metadata_t metadata, char **error_out) {
- box_apple_tls_state_t loaded_state = {0};
- if (!box_apple_tls_state_load_sec_metadata(metadata, &loaded_state, error_out)) {
- return false;
- }
- box_apple_tls_state_reset(&client->state);
- client->state = loaded_state;
- memset(&loaded_state, 0, sizeof(box_apple_tls_state_t));
- return true;
-}
-
static bool box_apple_tls_state_copy(const box_apple_tls_state_t *source, box_apple_tls_state_t *destination) {
memset(destination, 0, sizeof(box_apple_tls_state_t));
destination->version = source->version;
@@ -382,6 +285,41 @@ oom:
return false;
}
+// Captures TLS negotiation results from the verify block. The sec_metadata
+// exposed here is live for the duration of the handshake; the one retrieved
+// after nw_connection_state_ready may return stale ALPN/server_name buffers.
+static void box_apple_tls_state_load(sec_protocol_metadata_t sec_metadata, box_apple_tls_state_t *state) {
+ state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata);
+ state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata);
+ state->alpn = box_apple_tls_metadata_copy_negotiated_protocol(sec_metadata);
+ state->server_name = box_apple_tls_metadata_copy_server_name(sec_metadata);
+
+ NSMutableData *chain_data = [NSMutableData data];
+ sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) {
+ SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate);
+ if (certificate_ref == NULL) {
+ return;
+ }
+ CFDataRef certificate_data = SecCertificateCopyData(certificate_ref);
+ CFRelease(certificate_ref);
+ if (certificate_data == NULL) {
+ return;
+ }
+ uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data);
+ uint32_t network_len = htonl(certificate_len);
+ [chain_data appendBytes:&network_len length:sizeof(network_len)];
+ [chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len];
+ CFRelease(certificate_data);
+ });
+ if (chain_data.length > 0) {
+ state->peer_cert_chain = malloc(chain_data.length);
+ if (state->peer_cert_chain != NULL) {
+ memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length);
+ state->peer_cert_chain_len = chain_data.length;
+ }
+ }
+}
+
box_apple_tls_client_t *box_apple_tls_client_create(
int connected_socket,
const char *server_name,
@@ -431,17 +369,10 @@ box_apple_tls_client_t *box_apple_tls_client_create(
}
sec_protocol_options_set_peer_authentication_required(sec_options, !insecure);
sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) {
- if (client->ready_error == NULL) {
- char *local_error = NULL;
- if (!box_apple_tls_client_capture_state(client, metadata, &local_error)) {
- client->ready_error = local_error;
- }
+ if (client->state.version == 0) {
+ box_apple_tls_state_load(metadata, &client->state);
}
- if (insecure) {
- complete(true);
- return;
- }
- complete(box_evaluate_trust(trust, anchors, anchor_only, verifyDate));
+ complete(insecure || box_evaluate_trust(trust, anchors, anchor_only, verifyDate));
}, box_apple_tls_client_queue(client));
}, NW_PARAMETERS_DEFAULT_CONFIGURATION);
@@ -466,11 +397,11 @@ box_apple_tls_client_t *box_apple_tls_client_create(
switch (state) {
case nw_connection_state_ready:
if (!atomic_load(&client->ready_done)) {
- bool state_loaded = client->state.version != 0;
- if (!state_loaded && client->ready_error == NULL) {
+ if (client->state.version == 0) {
box_set_error_message(&client->ready_error, "apple TLS: metadata unavailable");
+ } else {
+ atomic_store(&client->ready, true);
}
- atomic_store(&client->ready, state_loaded);
atomic_store(&client->ready_done, true);
dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client));
}
diff --git a/sing-box/common/tls/client.go b/sing-box/common/tls/client.go
index 22215176e0..b5b975bf23 100644
--- a/sing-box/common/tls/client.go
+++ b/sing-box/common/tls/client.go
@@ -48,16 +48,7 @@ func applyTLSSpoof(conn net.Conn, spoof string, method tlsspoof.Method) (net.Con
if spoof == "" {
return conn, nil
}
- spoofer, err := tlsspoof.NewSpoofer(conn, method)
- if err != nil {
- return nil, err
- }
- spoofConn, err := tlsspoof.NewConn(conn, spoofer, spoof)
- if err != nil {
- spoofer.Close()
- return nil, err
- }
- return spoofConn, nil
+ return tlsspoof.NewConn(conn, method, spoof)
}
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
diff --git a/sing-box/common/tlsspoof/conn_test.go b/sing-box/common/tlsspoof/conn_test.go
index d27c53a989..b41cf54753 100644
--- a/sing-box/common/tlsspoof/conn_test.go
+++ b/sing-box/common/tlsspoof/conn_test.go
@@ -58,7 +58,7 @@ func TestConn_Write_InjectsThenForwards(t *testing.T) {
client, server := net.Pipe()
spoofer := &fakeSpoofer{}
- wrapped, err := NewConn(client, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(client, spoofer, "letsencrypt.org")
require.NoError(t, err)
serverRead := make(chan []byte, 1)
@@ -87,7 +87,7 @@ func TestConn_Write_SecondWriteDoesNotInject(t *testing.T) {
client, server := net.Pipe()
spoofer := &fakeSpoofer{}
- wrapped, err := NewConn(client, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(client, spoofer, "letsencrypt.org")
require.NoError(t, err)
serverRead := make(chan []byte, 1)
@@ -115,7 +115,7 @@ func TestConn_Write_SurfacesCloseError(t *testing.T) {
defer client.Close()
defer server.Close()
spoofer := &fakeSpoofer{closeErr: errSpoofClose}
- wrapped, err := NewConn(client, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(client, spoofer, "letsencrypt.org")
require.NoError(t, err)
go func() { _, _ = io.ReadAll(server) }()
@@ -130,7 +130,7 @@ func TestConn_NewConn_RejectsEmptySNI(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
- _, err := NewConn(client, &fakeSpoofer{}, "")
+ _, err := newConn(client, &fakeSpoofer{}, "")
require.Error(t, err, "empty SNI must fail at construction")
}
@@ -195,7 +195,7 @@ func TestConn_StackedWithRecordFragment(t *testing.T) {
fragConn := tf.NewConn(client, context.Background(), false, true, time.Millisecond)
spoofer := &fakeSpoofer{}
- wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org")
require.NoError(t, err)
serverRead := make(chan []byte, 1)
@@ -238,7 +238,7 @@ func TestConn_StackedWithPacketFragment(t *testing.T) {
rc := &recordingConn{Conn: client}
fragConn := tf.NewConn(rc, context.Background(), true, false, time.Millisecond)
spoofer := &fakeSpoofer{}
- wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org")
require.NoError(t, err)
serverRead := make(chan []byte, 1)
@@ -273,7 +273,7 @@ func TestConn_StackedWithBothFragment(t *testing.T) {
rc := &recordingConn{Conn: client}
fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond)
spoofer := &fakeSpoofer{}
- wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org")
require.NoError(t, err)
serverRead := make(chan []byte, 1)
@@ -330,7 +330,7 @@ func TestConn_StackedInjectionOrder(t *testing.T) {
rc := &recordingConn{Conn: client, timeline: &timeline}
fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond)
spoofer := &trackingSpoofer{timeline: &timeline}
- wrapped, err := NewConn(fragConn, spoofer, "letsencrypt.org")
+ wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org")
require.NoError(t, err)
serverRead := make(chan []byte, 1)
diff --git a/sing-box/common/tlsspoof/integration_tls_test.go b/sing-box/common/tlsspoof/integration_tls_test.go
index 669df7eb2f..d179c3841c 100644
--- a/sing-box/common/tlsspoof/integration_tls_test.go
+++ b/sing-box/common/tlsspoof/integration_tls_test.go
@@ -86,9 +86,7 @@ func TestIntegrationConn_RealTLSHandshake(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { raw.Close() })
- spoofer, err := NewSpoofer(raw, MethodWrongSequence)
- require.NoError(t, err)
- wrapped, err := NewConn(raw, spoofer, fakeSNI)
+ wrapped, err := NewConn(raw, MethodWrongSequence, fakeSNI)
require.NoError(t, err)
clientConfig := &tls.Config{
diff --git a/sing-box/common/tlsspoof/integration_unix_test.go b/sing-box/common/tlsspoof/integration_unix_test.go
index c5d26128a5..0f4585fd82 100644
--- a/sing-box/common/tlsspoof/integration_unix_test.go
+++ b/sing-box/common/tlsspoof/integration_unix_test.go
@@ -15,7 +15,7 @@ import (
func TestIntegrationSpoofer_WrongChecksum(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServer(t)
- spoofer, err := NewSpoofer(client, MethodWrongChecksum)
+ spoofer, err := newRawSpoofer(client, MethodWrongChecksum)
require.NoError(t, err)
defer spoofer.Close()
@@ -31,7 +31,7 @@ func TestIntegrationSpoofer_WrongChecksum(t *testing.T) {
func TestIntegrationSpoofer_WrongSequence(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServer(t)
- spoofer, err := NewSpoofer(client, MethodWrongSequence)
+ spoofer, err := newRawSpoofer(client, MethodWrongSequence)
require.NoError(t, err)
defer spoofer.Close()
@@ -47,7 +47,7 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) {
func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServerIPv6(t)
- spoofer, err := NewSpoofer(client, MethodWrongChecksum)
+ spoofer, err := newRawSpoofer(client, MethodWrongChecksum)
require.NoError(t, err)
defer spoofer.Close()
@@ -63,7 +63,7 @@ func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) {
func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServerIPv6(t)
- spoofer, err := NewSpoofer(client, MethodWrongSequence)
+ spoofer, err := newRawSpoofer(client, MethodWrongSequence)
require.NoError(t, err)
defer spoofer.Close()
@@ -130,9 +130,7 @@ func runFakeAndRealHaveDistinctSNIs(t *testing.T, network, address, fakeSNI stri
listener.Close()
})
- spoofer, err := NewSpoofer(client, MethodWrongSequence)
- require.NoError(t, err)
- wrapped, err := NewConn(client, spoofer, fakeSNI)
+ wrapped, err := NewConn(client, MethodWrongSequence, fakeSNI)
require.NoError(t, err)
payload, err := hex.DecodeString(realClientHello)
@@ -185,9 +183,7 @@ func runInjectsThenForwardsRealCH(t *testing.T, network, address string) {
listener.Close()
})
- spoofer, err := NewSpoofer(client, MethodWrongSequence)
- require.NoError(t, err)
- wrapped, err := NewConn(client, spoofer, "letsencrypt.org")
+ wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org")
require.NoError(t, err)
payload, err := hex.DecodeString(realClientHello)
diff --git a/sing-box/common/tlsspoof/integration_windows_test.go b/sing-box/common/tlsspoof/integration_windows_test.go
index 3b8e3c549e..b0461a31b2 100644
--- a/sing-box/common/tlsspoof/integration_windows_test.go
+++ b/sing-box/common/tlsspoof/integration_windows_test.go
@@ -12,11 +12,11 @@ import (
"github.com/stretchr/testify/require"
)
-func newSpoofer(t *testing.T, conn net.Conn, method Method) Spoofer {
+func newSpoofer(t *testing.T, conn net.Conn, method Method) rawSpoofer {
t.Helper()
- spoofer, err := NewSpoofer(conn, method)
+ s, err := newRawSpoofer(conn, method)
require.NoError(t, err)
- return spoofer
+ return s
}
// Basic lifecycle: opening a spoofer against a live TCP conn installs
@@ -71,8 +71,7 @@ func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { client.Close() })
- spoofer := newSpoofer(t, client, MethodWrongSequence)
- wrapped, err := NewConn(client, spoofer, "letsencrypt.org")
+ wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org")
require.NoError(t, err)
payload, err := hex.DecodeString(realClientHello)
@@ -94,7 +93,7 @@ func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) {
// Inject before any kernel payload: stages the fake, then Write flushes
// the real CH. Same terminal expectation as the Conn variant but via the
-// Spoofer primitive directly.
+// raw spoofer primitive directly.
func TestIntegrationSpooferInjectThenWrite(t *testing.T) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
require.NoError(t, err)
diff --git a/sing-box/common/tlsspoof/raw_darwin.go b/sing-box/common/tlsspoof/raw_darwin.go
index 73b11dd37a..ab31687692 100644
--- a/sing-box/common/tlsspoof/raw_darwin.go
+++ b/sing-box/common/tlsspoof/raw_darwin.go
@@ -67,7 +67,7 @@ type darwinSpoofer struct {
receiveNext uint32
}
-func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
+func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) {
_, src, dst, err := tcpEndpoints(conn)
if err != nil {
return nil, err
diff --git a/sing-box/common/tlsspoof/raw_linux.go b/sing-box/common/tlsspoof/raw_linux.go
index 70774c297c..f82fbc9efb 100644
--- a/sing-box/common/tlsspoof/raw_linux.go
+++ b/sing-box/common/tlsspoof/raw_linux.go
@@ -29,7 +29,7 @@ type linuxSpoofer struct {
receiveNext uint32
}
-func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
+func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) {
tcpConn, src, dst, err := tcpEndpoints(conn)
if err != nil {
return nil, err
diff --git a/sing-box/common/tlsspoof/raw_stub.go b/sing-box/common/tlsspoof/raw_stub.go
index a2da87d6b3..7edf2441a6 100644
--- a/sing-box/common/tlsspoof/raw_stub.go
+++ b/sing-box/common/tlsspoof/raw_stub.go
@@ -10,6 +10,6 @@ import (
const PlatformSupported = false
-func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
+func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) {
return nil, E.New("tls_spoof: unsupported platform")
}
diff --git a/sing-box/common/tlsspoof/raw_windows.go b/sing-box/common/tlsspoof/raw_windows.go
index 4c6d130fbf..9f6553f1b8 100644
--- a/sing-box/common/tlsspoof/raw_windows.go
+++ b/sing-box/common/tlsspoof/raw_windows.go
@@ -41,7 +41,7 @@ type windowsSpoofer struct {
runErr atomic.Pointer[error]
}
-func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) {
+func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) {
_, src, dst, err := tcpEndpoints(conn)
if err != nil {
return nil, err
diff --git a/sing-box/common/tlsspoof/spoof.go b/sing-box/common/tlsspoof/spoof.go
index 249af08f6c..1bca5693fe 100644
--- a/sing-box/common/tlsspoof/spoof.go
+++ b/sing-box/common/tlsspoof/spoof.go
@@ -40,23 +40,32 @@ func (m Method) String() string {
}
}
-type Spoofer interface {
+type rawSpoofer interface {
Inject(payload []byte) error
Close() error
}
-func NewSpoofer(conn net.Conn, method Method) (Spoofer, error) {
- return newRawSpoofer(conn, method)
-}
-
type Conn struct {
net.Conn
- spoofer Spoofer
+ spoofer rawSpoofer
fakeHello []byte
injected bool
}
-func NewConn(conn net.Conn, spoofer Spoofer, fakeSNI string) (*Conn, error) {
+func NewConn(conn net.Conn, method Method, fakeSNI string) (*Conn, error) {
+ spoofer, err := newRawSpoofer(conn, method)
+ if err != nil {
+ return nil, err
+ }
+ result, err := newConn(conn, spoofer, fakeSNI)
+ if err != nil {
+ spoofer.Close()
+ return nil, err
+ }
+ return result, nil
+}
+
+func newConn(conn net.Conn, spoofer rawSpoofer, fakeSNI string) (*Conn, error) {
fakeHello, err := buildFakeClientHello(fakeSNI)
if err != nil {
return nil, E.Cause(err, "tls_spoof: build fake ClientHello")
@@ -88,7 +97,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
func (c *Conn) Close() error {
return E.Append(c.Conn.Close(), c.spoofer.Close(), func(e error) error {
- return E.Cause(e, "close spoofer")
+ return E.Cause(e, "tls_spoof: close spoofer")
})
}
diff --git a/sing-box/docs/changelog.md b/sing-box/docs/changelog.md
index 590d7bf1c8..1f88e21181 100644
--- a/sing-box/docs/changelog.md
+++ b/sing-box/docs/changelog.md
@@ -2,6 +2,10 @@
icon: material/alert-decagram
---
+#### 1.14.0-alpha.14
+
+* Fixes and improvements
+
#### 1.14.0-alpha.13
* Unify HTTP client **1**
diff --git a/sing-box/go.mod b/sing-box/go.mod
index d0f4f0b9ad..6fda1a8eef 100644
--- a/sing-box/go.mod
+++ b/sing-box/go.mod
@@ -37,7 +37,7 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
- github.com/sagernet/sing v0.8.7-0.20260416084641-60846c117dd8
+ github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386
github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6
diff --git a/sing-box/go.sum b/sing-box/go.sum
index 36c14d0bdd..77a3ad21ab 100644
--- a/sing-box/go.sum
+++ b/sing-box/go.sum
@@ -242,8 +242,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
-github.com/sagernet/sing v0.8.7-0.20260416084641-60846c117dd8 h1:gBTi6DvU/F/ZKcq9HvEWpT/DhpNU68FTGHK2c+m80vo=
-github.com/sagernet/sing v0.8.7-0.20260416084641-60846c117dd8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386 h1:gNonzIaoKi4MiEmK/5iJI+RoGof1PtYqzSaNE/lmWLw=
+github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9 h1:wWJexq4V7ope1c4XYSqT/hydqd9omdtbDRvSZIOhjfY=
github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile
index 1b9d47e3aa..67f4197a0c 100644
--- a/small/v2ray-geodata/Makefile
+++ b/small/v2ray-geodata/Makefile
@@ -21,13 +21,13 @@ define Download/geoip
HASH:=16dbd19ff8dddb69960f313a3b0c0623cae82dc9725687110c28740226d3b285
endef
-GEOSITE_VER:=20260417103656
+GEOSITE_VER:=20260418094517
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
- HASH:=e99ce48793a85e3ae66d0234414d20c7c469c5adfc123d5d7becd027c40f4dfa
+ HASH:=5ff843a4c8ddcc8a7864301c6a84c50ac190ec10a99c0b66c865861d68fb4c30
endef
GEOSITE_IRAN_VER:=202604130112
diff --git a/v2rayn/v2rayN/ServiceLib/Global.cs b/v2rayn/v2rayN/ServiceLib/Global.cs
index 267ec60a2f..7c0f98d806 100644
--- a/v2rayn/v2rayN/ServiceLib/Global.cs
+++ b/v2rayn/v2rayN/ServiceLib/Global.cs
@@ -94,7 +94,7 @@ public class Global
public const string SingboxHostsDNSTag = "hosts_dns";
public const string SingboxFakeDNSTag = "fake_dns";
- public const int Hysteria2DefaultHopInt = 10;
+ public const int Hysteria2DefaultHopInt = 30;
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
diff --git a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index eab544dabc..da7258a7d2 100644
--- a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -77,10 +77,11 @@ public static class ConfigHandler
Tti = 50,
UplinkCapacity = 12,
DownlinkCapacity = 100,
- ReadBufferSize = 2,
- WriteBufferSize = 2,
- Congestion = false
+ CwndMultiplier = 1,
+ MaxSendingWindow = 2 * 1024 * 1024,
};
+ config.KcpItem.CwndMultiplier = config.KcpItem.CwndMultiplier <= 0 ? 1 : config.KcpItem.CwndMultiplier;
+ config.KcpItem.MaxSendingWindow = config.KcpItem.MaxSendingWindow <= 0 ? (2 * 1024 * 1024) : config.KcpItem.MaxSendingWindow;
config.GrpcItem ??= new GrpcItem
{
IdleTimeout = 60,
diff --git a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs
index fa766f0d65..1988d3b713 100644
--- a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -49,11 +49,9 @@ public class KcpItem
public int DownlinkCapacity { get; set; }
- public bool Congestion { get; set; }
+ public int CwndMultiplier { get; set; }
- public int ReadBufferSize { get; set; }
-
- public int WriteBufferSize { get; set; }
+ public int MaxSendingWindow { get; set; }
}
[Serializable]
@@ -197,7 +195,7 @@ public class HysteriaItem
{
public int UpMbps { get; set; }
public int DownMbps { get; set; }
- public int HopInterval { get; set; } = 30;
+ public int HopInterval { get; set; } = Global.Hysteria2DefaultHopInt;
}
[Serializable]
diff --git a/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs
index a5ac2e24de..909a37fb1d 100644
--- a/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs
+++ b/v2rayn/v2rayN/ServiceLib/Models/V2rayConfig.cs
@@ -409,11 +409,9 @@ public class KcpSettings4Ray
public int downlinkCapacity { get; set; }
- public bool congestion { get; set; }
+ public int cwndMultiplier { get; set; }
- public int readBufferSize { get; set; }
-
- public int writeBufferSize { get; set; }
+ public int maxSendingWindow { get; set; }
}
public class WsSettings4Ray
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
index 234cdef3cd..0743b6c7ff 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
@@ -4681,7 +4681,16 @@ namespace ServiceLib.Resx {
}
///
- /// 查找类似 XHTTP Extra raw JSON, format: { XHTTP Object } 的本地化字符串。
+ /// 查找类似 XHTTP Extra 的本地化字符串。
+ ///
+ public static string TransportExtra {
+ get {
+ return ResourceManager.GetString("TransportExtra", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Raw JSON, format: { XHTTP Object } 的本地化字符串。
///
public static string TransportExtraTip {
get {
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
index 08aaf89314..84f9722e2d 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
@@ -1321,7 +1321,7 @@
حالت xhttp
- جیسون خام XHTTP Extra, فرمت: { XHTTPObject }
+ Raw JSON, format: { XHTTP Object }
هنگام بستن پنجره در سینی پنهان شوید
@@ -1704,4 +1704,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Host
+
+ XHTTP Extra
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx
index 8540727fe7..84033130c2 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.fr.resx
@@ -1327,7 +1327,7 @@
Mode XHTTP
- JSON brut XHTTP Extra, format : { XHTTPObject }
+ Raw JSON, format: { XHTTP Object }
Masquer dans la barre d’état à la fermeture de la fenêtre
@@ -1707,4 +1707,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Host
-
+
+ XHTTP Extra
+
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx
index f761cca9dd..e58ae6f06c 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.hu.resx
@@ -1321,7 +1321,7 @@
xhttp mód
- XHTTP Extra nyers JSON, formátum: { XHTTP Objektum }
+ Raw JSON, format: { XHTTP Object }
Ablak bezárásakor a tálcára rejtés
@@ -1704,4 +1704,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Host
+
+ XHTTP Extra
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx
index d30c3f3183..b4120d3c71 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx
@@ -1330,7 +1330,7 @@
xhttp mode
- XHTTP Extra raw JSON, format: { XHTTP Object }
+ Raw JSON, format: { XHTTP Object }
Hide to tray when closing the window
@@ -1710,4 +1710,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Host
+
+ XHTTP Extra
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx
index 1521e67ce6..4d30949c4b 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.ru.resx
@@ -1321,7 +1321,7 @@
XHTTP-режим
- Дополнительный сырой JSON для XHTTP, формат: { XHTTP Object }
+ Raw JSON, format: { XHTTP Object }
Сворачивать в трей при закрытии окна
@@ -1704,4 +1704,7 @@
Host
+
+ XHTTP Extra
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
index 379fb4bad6..b47b832e1d 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
@@ -1327,7 +1327,7 @@
XHTTP 模式
- XHTTP Extra 原始 JSON,格式: { XHTTPObject }
+ 原始 JSON,格式: { XHTTPObject }
关闭窗口时隐藏至托盘
@@ -1707,4 +1707,7 @@
Host
+
+ XHTTP Extra
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
index a903d16874..7ba89b3381 100644
--- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
+++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
@@ -1318,7 +1318,7 @@
xhttp 模式
- XHTTP Extra 原始 JSON,格式: { XHTTPObject }
+ 原始 JSON,格式: { XHTTPObject }
關閉視窗時隱藏至托盤
@@ -1701,4 +1701,7 @@
Host
+
+ XHTTP Extra
+
\ No newline at end of file
diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs
index 6e563f0fc9..e6bc48cb87 100644
--- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs
+++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs
@@ -5,12 +5,18 @@ public partial class CoreConfigV2rayService
private string ApplyFullConfigTemplate()
{
var fullConfigTemplate = context.FullConfigTemplate;
- if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
+ if (fullConfigTemplate is not { Enabled: true })
{
return JsonUtils.Serialize(_coreConfig);
}
- var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config);
+ var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
+ if (fullConfigTemplateItem.IsNullOrEmpty())
+ {
+ return JsonUtils.Serialize(_coreConfig);
+ }
+
+ var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null)
{
return JsonUtils.Serialize(_coreConfig);
diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
index 6b0d335daa..7faae5b1b0 100644
--- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
+++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
@@ -370,11 +370,11 @@ public partial class CoreConfigV2rayService
try
{
var item = context.RawDnsItem;
- var normalDNS = item?.NormalDNS;
+ var customDNS = context.IsTunEnabled ? item?.TunDNS : item?.NormalDNS;
var domainStrategy4Freedom = item?.DomainStrategy4Freedom;
- if (normalDNS.IsNullOrEmpty())
+ if (customDNS.IsNullOrEmpty())
{
- normalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName);
+ customDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName);
}
//Outbound Freedom domainStrategy
@@ -389,11 +389,11 @@ public partial class CoreConfigV2rayService
}
}
- var obj = JsonUtils.ParseJson(normalDNS);
+ var obj = JsonUtils.ParseJson(customDNS);
if (obj is null)
{
List servers = [];
- var arrDNS = normalDNS.Split(',');
+ var arrDNS = customDNS.Split(',');
foreach (var str in arrDNS)
{
servers.Add(str);
diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
index a0d4099fa8..d40ed8a377 100644
--- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
+++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
@@ -477,9 +477,8 @@ public partial class CoreConfigV2rayService
kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity;
kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity;
- kcpSettings.congestion = _config.KcpItem.Congestion;
- kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize;
- kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize;
+ kcpSettings.cwndMultiplier = _config.KcpItem.CwndMultiplier;
+ kcpSettings.maxSendingWindow = _config.KcpItem.MaxSendingWindow;
var kcpFinalmask = new Finalmask4Ray();
if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header))
{
diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
index 9fbb612578..0264bc1fbd 100644
--- a/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
+++ b/v2rayn/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
@@ -292,7 +292,7 @@ public class AddServerViewModel : MyReactiveObject
SalamanderPass = protocolExtra?.SalamanderPass ?? string.Empty;
UpMbps = protocolExtra?.UpMbps;
DownMbps = protocolExtra?.DownMbps;
- HopInterval = protocolExtra?.HopInterval.IsNullOrEmpty() ?? true ? Global.Hysteria2DefaultHopInt.ToString() : protocolExtra.HopInterval;
+ HopInterval = protocolExtra?.HopInterval ?? string.Empty;
VmessSecurity = protocolExtra?.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity;
VlessEncryption = protocolExtra?.VlessEncryption.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None;
SsMethod = protocolExtra?.SsMethod ?? string.Empty;
diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs
index 00178d1797..25fbe4b92b 100644
--- a/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs
+++ b/v2rayn/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs
@@ -20,6 +20,7 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public string DomainStrategy4FreedomCompatible { get; set; }
[Reactive] public string DomainDNSAddressCompatible { get; set; }
[Reactive] public string NormalDNSCompatible { get; set; }
+ [Reactive] public string TunDNSCompatible { get; set; }
[Reactive] public string DomainStrategy4Freedom2Compatible { get; set; }
[Reactive] public string DomainDNSAddress2Compatible { get; set; }
@@ -43,6 +44,7 @@ public class DNSSettingViewModel : MyReactiveObject
ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () =>
{
NormalDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName);
+ TunDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName);
await Task.CompletedTask;
});
@@ -84,6 +86,7 @@ public class DNSSettingViewModel : MyReactiveObject
DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty;
DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty;
NormalDNSCompatible = item1?.NormalDNS ?? string.Empty;
+ TunDNSCompatible = item1?.TunDNS ?? string.Empty;
var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
SBCustomDNSEnableCompatible = item2.Enabled;
@@ -124,6 +127,21 @@ public class DNSSettingViewModel : MyReactiveObject
}
}
}
+ if (TunDNSCompatible.IsNotEmpty())
+ {
+ var obj = JsonUtils.ParseJson(TunDNSCompatible);
+ if (obj != null && obj["servers"] != null)
+ {
+ }
+ else
+ {
+ if (TunDNSCompatible.Contains('{') || TunDNSCompatible.Contains('}'))
+ {
+ NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText);
+ return;
+ }
+ }
+ }
if (NormalDNS2Compatible.IsNotEmpty())
{
var obj2 = JsonUtils.Deserialize(NormalDNS2Compatible);
@@ -149,6 +167,7 @@ public class DNSSettingViewModel : MyReactiveObject
item1.DomainDNSAddress = DomainDNSAddressCompatible;
item1.UseSystemHosts = UseSystemHostsCompatible;
item1.NormalDNS = NormalDNSCompatible;
+ item1.TunDNS = TunDNSCompatible;
await ConfigHandler.SaveDNSItems(_config, item1);
var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs
index 3a50b52ede..78cd10a260 100644
--- a/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs
+++ b/v2rayn/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs
@@ -13,6 +13,9 @@ public class FullConfigTemplateViewModel : MyReactiveObject
[Reactive]
public string FullConfigTemplate4Ray { get; set; }
+ [Reactive]
+ public string FullTunConfigTemplate4Ray { get; set; }
+
[Reactive]
public string FullConfigTemplate4Singbox { get; set; }
@@ -50,10 +53,15 @@ public class FullConfigTemplateViewModel : MyReactiveObject
private async Task Init()
{
var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
- EnableFullConfigTemplate4Ray = item?.Enabled ?? false;
- FullConfigTemplate4Ray = item?.Config ?? string.Empty;
- AddProxyOnly4Ray = item?.AddProxyOnly ?? false;
- ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty;
+ if (item == null)
+ {
+ return;
+ }
+ EnableFullConfigTemplate4Ray = item.Enabled;
+ FullConfigTemplate4Ray = item.Config ?? string.Empty;
+ FullTunConfigTemplate4Ray = item.TunConfig ?? string.Empty;
+ AddProxyOnly4Ray = item.AddProxyOnly ?? false;
+ ProxyDetour4Ray = item.ProxyDetour ?? string.Empty;
var item2 = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box);
EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false;
@@ -82,10 +90,13 @@ public class FullConfigTemplateViewModel : MyReactiveObject
private async Task SaveXrayConfigAsync()
{
var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
+ if (item == null)
+ {
+ return false;
+ }
item.Enabled = EnableFullConfigTemplate4Ray;
- item.Config = null;
-
item.Config = FullConfigTemplate4Ray;
+ item.TunConfig = FullTunConfigTemplate4Ray;
item.AddProxyOnly = AddProxyOnly4Ray;
item.ProxyDetour = ProxyDetour4Ray;
@@ -97,10 +108,11 @@ public class FullConfigTemplateViewModel : MyReactiveObject
private async Task SaveSingboxConfigAsync()
{
var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box);
+ if (item == null)
+ {
+ return false;
+ }
item.Enabled = EnableFullConfigTemplate4Singbox;
- item.Config = null;
- item.TunConfig = null;
-
item.Config = FullConfigTemplate4Singbox;
item.TunConfig = FullTunConfigTemplate4Singbox;
diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
index 1d2bb899fe..56a579dbc2 100644
--- a/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
+++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
@@ -814,17 +814,17 @@
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Top"
- Text="{x:Static resx:ResUI.TransportExtraTip}"
+ Text="{x:Static resx:ResUI.TransportExtra}"
TextWrapping="Wrap" />
-
+ IsExpanded="True">
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs
index dd39e22978..e2ea1201b0 100644
--- a/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs
+++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs
@@ -52,6 +52,7 @@ public partial class DNSSettingWindow : WindowBase
this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.TunDNSCompatible, v => v.txttunDNSCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables);
diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml
index 1a2b482c0d..7ff499374f 100644
--- a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml
+++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml
@@ -90,13 +90,26 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs
index bfe0c2c580..6ea6e994ae 100644
--- a/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs
+++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs
@@ -12,13 +12,14 @@ public partial class FullConfigTemplateWindow : WindowBase Close();
+ btnCancel.Click += (_, _) => Close();
ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler);
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Ray, v => v.rayFullConfigTemplateEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FullConfigTemplate4Ray, v => v.rayFullConfigTemplate.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.FullTunConfigTemplate4Ray, v => v.rayFullTunConfigTemplate.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AddProxyOnly4Ray, v => v.togAddProxyProtocolOutboundOnly4Ray.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ProxyDetour4Ray, v => v.txtProxyDetour4Ray.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Singbox, v => v.sbFullConfigTemplateEnable.IsChecked).DisposeWith(disposables);
diff --git a/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml
index 25a1234b8d..ec86e90490 100644
--- a/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml
+++ b/v2rayn/v2rayN/v2rayN/Views/AddServerWindow.xaml
@@ -1071,7 +1071,7 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Top"
Style="{StaticResource ToolbarTextBlock}"
- Text="{x:Static resx:ResUI.TransportExtraTip}"
+ Text="{x:Static resx:ResUI.TransportExtra}"
TextWrapping="Wrap" />
diff --git a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml
index 8788e97d80..d27188a4a8 100644
--- a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml
+++ b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml
@@ -453,16 +453,37 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs
index 2710b102b8..a7f1bd12d4 100644
--- a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs
+++ b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs
@@ -50,6 +50,7 @@ public partial class DNSSettingWindow
this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.TunDNSCompatible, v => v.txttunDNSCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables);
diff --git a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml
index d3ca8af7bc..4b9953d7c0 100644
--- a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml
+++ b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml
@@ -107,16 +107,37 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs
index a9f95a5359..031dc40abb 100644
--- a/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs
+++ b/v2rayn/v2rayN/v2rayN/Views/FullConfigTemplateWindow.xaml.cs
@@ -17,6 +17,7 @@ public partial class FullConfigTemplateWindow
{
this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Ray, v => v.rayFullConfigTemplateEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FullConfigTemplate4Ray, v => v.rayFullConfigTemplate.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.FullTunConfigTemplate4Ray, v => v.rayFullTunConfigTemplate.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AddProxyOnly4Ray, v => v.togAddProxyProtocolOutboundOnly4Ray.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ProxyDetour4Ray, v => v.txtProxyDetour4Ray.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFullConfigTemplate4Singbox, v => v.sbFullConfigTemplateEnable.IsChecked).DisposeWith(disposables);
diff --git a/v2rayng/AndroidLibXrayLite/go.mod b/v2rayng/AndroidLibXrayLite/go.mod
index c30c8d27bb..4ae625cdc4 100644
--- a/v2rayng/AndroidLibXrayLite/go.mod
+++ b/v2rayng/AndroidLibXrayLite/go.mod
@@ -3,7 +3,7 @@ module github.com/2dust/AndroidLibXrayLite
go 1.26
require (
- github.com/xtls/xray-core v1.260327.1-0.20260415235634-c5edc122b70e
+ github.com/xtls/xray-core v1.260327.1-0.20260417230405-b4650360d6a0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
)
@@ -19,7 +19,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
- github.com/pires/go-proxyproto v0.11.0 // indirect
+ github.com/pires/go-proxyproto v0.12.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
diff --git a/v2rayng/AndroidLibXrayLite/go.sum b/v2rayng/AndroidLibXrayLite/go.sum
index daf965fd7e..c63e1906d9 100644
--- a/v2rayng/AndroidLibXrayLite/go.sum
+++ b/v2rayng/AndroidLibXrayLite/go.sum
@@ -40,8 +40,8 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
-github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
+github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
+github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -62,8 +62,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
-github.com/xtls/xray-core v1.260327.1-0.20260415235634-c5edc122b70e h1:Ndk6i7q2rExWGsXzp1KsUvqkY4Id+NgpGr73Sg/x4lM=
-github.com/xtls/xray-core v1.260327.1-0.20260415235634-c5edc122b70e/go.mod h1:nFFTSnhpW0qTKUOeABKy3XAxdZ9WRuyhP4dEtPy1Qps=
+github.com/xtls/xray-core v1.260327.1-0.20260417230405-b4650360d6a0 h1:9cf2NebR+d4bytVztd5CM3PAfJyfJT4WHucjCYrLv+g=
+github.com/xtls/xray-core v1.260327.1-0.20260417230405-b4650360d6a0/go.mod h1:DowU2gGzHZ9NB9kG2ykRx8SQ09KpSyNjLrdc8XPHVm8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
diff --git a/v2rayng/V2rayNG/app/build.gradle.kts b/v2rayng/V2rayNG/app/build.gradle.kts
index 61caf566b6..9679362c01 100644
--- a/v2rayng/V2rayNG/app/build.gradle.kts
+++ b/v2rayng/V2rayNG/app/build.gradle.kts
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 36
- versionCode = 720
- versionName = "2.1.0"
+ versionCode = 721
+ versionName = "2.1.1"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
diff --git a/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml b/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml
index 0382514337..9d2dd6e833 100644
--- a/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml
+++ b/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml
@@ -88,6 +88,9 @@
+
diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppPickerActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppPickerActivity.kt
new file mode 100644
index 0000000000..c3234b31ff
--- /dev/null
+++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppPickerActivity.kt
@@ -0,0 +1,179 @@
+package com.v2ray.ang.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.widget.SearchView
+import androidx.lifecycle.lifecycleScope
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityAppPickerBinding
+import com.v2ray.ang.dto.AppInfo
+import com.v2ray.ang.util.AppManagerUtil
+import com.v2ray.ang.util.LogUtil
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.text.Collator
+
+class AppPickerActivity : BaseActivity() {
+ companion object {
+ private const val EXTRA_SELECTED_PACKAGES = "selected_packages"
+ private const val EXTRA_PICKER_TITLE = "picker_title"
+
+ fun createIntent(
+ context: Context,
+ selectedPackages: Collection = emptyList(),
+ title: String? = null
+ ): Intent = Intent(context, AppPickerActivity::class.java).apply {
+ putStringArrayListExtra(EXTRA_SELECTED_PACKAGES, ArrayList(selectedPackages))
+ title?.let { putExtra(EXTRA_PICKER_TITLE, it) }
+ }
+
+ fun getSelectedPackages(intent: Intent?): List {
+ return intent?.getStringArrayListExtra(EXTRA_SELECTED_PACKAGES).orEmpty()
+ }
+ }
+
+ private val binding by lazy { ActivityAppPickerBinding.inflate(layoutInflater) }
+ private val initialSelectedPackages by lazy {
+ intent.getStringArrayListExtra(EXTRA_SELECTED_PACKAGES).orEmpty()
+ }
+ private val selectedPackages = LinkedHashSet()
+ private var appsAll: List = emptyList()
+ private val adapter = AppSelectorAdapter(selectedPackages)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = resolveScreenTitle())
+
+ selectedPackages.addAll(initialSelectedPackages)
+ setupRecyclerView()
+ loadApps()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_app_picker, menu)
+
+ val searchItem = menu.findItem(R.id.search_view)
+ if (searchItem != null) {
+ val searchView = searchItem.actionView as SearchView
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean = false
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ filterApps(newText.orEmpty())
+ return false
+ }
+ })
+ }
+
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.select_all -> {
+ selectAllVisible()
+ true
+ }
+
+ R.id.invert_selection -> {
+ invertVisibleSelection()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ override fun finish() {
+ setResult(
+ RESULT_OK,
+ Intent().apply {
+ putStringArrayListExtra(EXTRA_SELECTED_PACKAGES, getSelectedPackages())
+ }
+ )
+ super.finish()
+ }
+
+ private fun setupRecyclerView() {
+ binding.recyclerView.adapter = adapter
+ addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
+ }
+
+ private fun loadApps() {
+ showLoading()
+
+ lifecycleScope.launch {
+ try {
+ val apps = withContext(Dispatchers.IO) {
+ val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity)
+ sortApps(appsList)
+ }
+
+ appsAll = apps
+ updateDisplayedApps(apps)
+ } catch (e: Exception) {
+ LogUtil.e("AppPickerActivity", "Failed to load app list", e)
+ } finally {
+ hideLoading()
+ }
+ }
+ }
+
+ private fun filterApps(content: String) {
+ val key = content.uppercase()
+ val filteredApps = appsAll.filter { app ->
+ key.isEmpty() || matchesSearch(app, key)
+ }
+ updateDisplayedApps(filteredApps)
+ }
+
+ private fun sortApps(apps: List): List {
+ val collator = Collator.getInstance()
+ return apps.sortedWith { p1, p2 ->
+ val p1Selected = selectedPackages.contains(p1.packageName)
+ val p2Selected = selectedPackages.contains(p2.packageName)
+ when {
+ p1Selected && !p2Selected -> -1
+ !p1Selected && p2Selected -> 1
+ p1.isSystemApp && !p2.isSystemApp -> 1
+ !p1.isSystemApp && p2.isSystemApp -> -1
+ else -> collator.compare(p1.appName, p2.appName)
+ }
+ }
+ }
+
+ private fun matchesSearch(app: AppInfo, keyword: String): Boolean {
+ return app.appName.uppercase().contains(keyword) || app.packageName.uppercase().contains(keyword)
+ }
+
+ private fun updateDisplayedApps(apps: List) {
+ adapter.submitList(apps)
+ }
+
+ private fun selectAllVisible() {
+ adapter.apps.forEach { app -> selectedPackages.add(app.packageName) }
+ adapter.refreshSelection()
+ }
+
+ private fun invertVisibleSelection() {
+ adapter.apps.forEach { app ->
+ if (selectedPackages.contains(app.packageName)) {
+ selectedPackages.remove(app.packageName)
+ } else {
+ selectedPackages.add(app.packageName)
+ }
+ }
+ adapter.refreshSelection()
+ }
+
+ private fun getSelectedPackages(): ArrayList {
+ return ArrayList(selectedPackages.sorted())
+ }
+
+ private fun resolveScreenTitle(): String {
+ return intent.getStringExtra(EXTRA_PICKER_TITLE) ?: getString(R.string.per_app_proxy_settings)
+ }
+}
+
diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppSelectorAdapter.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppSelectorAdapter.kt
new file mode 100644
index 0000000000..719664a4c2
--- /dev/null
+++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AppSelectorAdapter.kt
@@ -0,0 +1,64 @@
+package com.v2ray.ang.ui
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
+import com.v2ray.ang.dto.AppInfo
+
+class AppSelectorAdapter(
+ private val selectedPackages: MutableSet
+) : RecyclerView.Adapter() {
+
+ var apps: List = emptyList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
+ val binding = ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return AppViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = apps.size
+
+ override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
+ holder.bind(apps[position])
+ }
+
+ fun submitList(newApps: List) {
+ apps = newApps
+ notifyDataSetChanged()
+ }
+
+ fun refreshSelection() {
+ notifyDataSetChanged()
+ }
+
+ inner class AppViewHolder(private val binding: ItemRecyclerBypassListBinding) : RecyclerView.ViewHolder(binding.root),
+ View.OnClickListener {
+ private lateinit var appInfo: AppInfo
+
+ fun bind(item: AppInfo) {
+ appInfo = item
+ binding.icon.setImageDrawable(item.appIcon)
+ binding.name.text = if (item.isSystemApp) {
+ String.format("** %s", item.appName)
+ } else {
+ item.appName
+ }
+ binding.packageName.text = item.packageName
+ binding.checkBox.isChecked = selectedPackages.contains(item.packageName)
+ itemView.setOnClickListener(this)
+ }
+
+ override fun onClick(v: View?) {
+ val packageName = appInfo.packageName
+ if (selectedPackages.contains(packageName)) {
+ selectedPackages.remove(packageName)
+ } else {
+ selectedPackages.add(packageName)
+ }
+ binding.checkBox.isChecked = selectedPackages.contains(packageName)
+ }
+ }
+}
+
diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt
index f20ce50482..27a6646a39 100644
--- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt
+++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt
@@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.ArrayAdapter
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
@@ -23,12 +24,19 @@ import kotlinx.coroutines.launch
class RoutingEditActivity : BaseActivity() {
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
private val position by lazy { intent.getIntExtra("position", -1) }
+ private val processPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ val selectedPackages = AppPickerActivity.getSelectedPackages(result.data)
+ binding.etProcess.text = Utils.getEditable(selectedPackages.joinToString(","))
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
setupOutboundTagInput()
+ setupProcessPicker()
val rulesetItem = SettingsManager.getRoutingRuleset(position)
if (rulesetItem != null) {
@@ -39,9 +47,31 @@ class RoutingEditActivity : BaseActivity() {
SettingsManager.canUseProcessRouting().let { canUse ->
binding.etProcess.isEnabled = canUse
+ binding.btnProcessPicker.isEnabled = canUse
}
}
+ private fun setupProcessPicker() {
+ binding.btnProcessPicker.setOnClickListener {
+ processPickerLauncher.launch(
+ AppPickerActivity.createIntent(
+ context = this,
+ selectedPackages = getSelectedProcessPackages(),
+ title = getString(R.string.routing_settings_process)
+ )
+ )
+ }
+ }
+
+ private fun getSelectedProcessPackages(): List {
+ return binding.etProcess.text
+ .toString()
+ .split(",")
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .distinct()
+ }
+
/**
* Sets up the AutoCompleteTextView for outbound tag:
* suggestions = built-in tags (proxy/direct/block) + all existing profile remarks.
diff --git a/v2rayng/V2rayNG/app/src/main/res/layout/activity_app_picker.xml b/v2rayng/V2rayNG/app/src/main/res/layout/activity_app_picker.xml
new file mode 100644
index 0000000000..5544419970
--- /dev/null
+++ b/v2rayng/V2rayNG/app/src/main/res/layout/activity_app_picker.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml b/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml
index d5ad605291..9db3ec8e1c 100644
--- a/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml
+++ b/v2rayng/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml
@@ -114,13 +114,34 @@
android:layout_height="wrap_content"
android:text="@string/routing_settings_process" />
-
+ android:layout_marginTop="@dimen/padding_spacing_dp8"
+ android:layout_marginBottom="@dimen/padding_spacing_dp8">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml b/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml
index be726045e4..55cba3dfc9 100644
--- a/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml
+++ b/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml
@@ -189,7 +189,7 @@
Добавить IPv6-адрес и маршруты в VPN-интерфейс
Предпочитать IPv6
- Предпочитать IPv6-адреса при разрешении доменных имён
+ Предпочитать IPv6-адреса при определении доменных имён
Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)
DNS
@@ -198,8 +198,10 @@
VPN обходит LAN
Адрес интерфейса VPN
+
VPN MTU (по умолчанию 1500)
+
Внутренняя DNS (необязательно)
DNS
@@ -221,7 +223,7 @@
Порт локального прокси
Порт локального прокси
- Динамически менять порт
+ Динамически менять порт локального прокси
Использовать случайный порт локального прокси при каждом создании соединения
Пользователь локального прокси (необязательно)
Имя пользователя
@@ -245,6 +247,7 @@
Общая вкладка групп
Показывать дополнительную вкладку со всеми профилями групп
+
Обратная связь
Предложить улучшение или сообщить об ошибке на GitHub
@@ -306,7 +309,7 @@
Импортировано профилей: %d
Экспортировано профилей: %d
Обновлено профилей: %d
- Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)
+ Обновлено профилей: %1$d (успешно: %2$d, ошибки: %3$d, пропущено: %4$d)
Нет подписок
Выбранный профиль не найден в текущей группе
Фрагмент недоступен
@@ -318,7 +321,7 @@
Доменная стратегия
Маршрутизация
- Разделяйте запятой (,). Укажите только одно: domain, ip или process
+ Через запятую (,)\nТолько что-то одно: домен, IP или процесс
Сохранить
Очистить
Настройка правил маршрутизации
@@ -331,6 +334,7 @@
Постоянное (сохранится при импорте правил)
Домен
IP
+ Процесс (название пакета — поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)
Порт
Протокол
[http,tls,bittorrent]
@@ -353,8 +357,8 @@
Невозможно импортировать подписку
Настройки фрагментирования
Фрагментирование пакетов
- Длина фрагмента (от - до)
- Интервал фрагментов (от - до)
+ Длина фрагмента (от — до)
+ Интервал фрагментов (от — до)
Использовать фрагментирование
Проверить обновление
@@ -429,6 +433,7 @@
- Определять и добавлять к узлам DNS
- Определять и заменять домен
+
- Наименьшая задержка
- Наименьшая нагрузка
diff --git a/xray-core/app/dns/dns.go b/xray-core/app/dns/dns.go
index 2f2729897b..ec350e8486 100644
--- a/xray-core/app/dns/dns.go
+++ b/xray-core/app/dns/dns.go
@@ -271,11 +271,11 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching
hasMatch := false
- MatchSlice := s.domainMatcher.Match(strings.ToLower(domain))
- sort.Slice(MatchSlice, func(i, j int) bool {
- return MatchSlice[i] < MatchSlice[j]
+ matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
+ sort.Slice(matchSlice, func(i, j int) bool {
+ return matchSlice[i] < matchSlice[j]
})
- for _, match := range MatchSlice {
+ for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
diff --git a/xray-core/app/dns/dns_test.go b/xray-core/app/dns/dns_test.go
index c065056e39..067ff44566 100644
--- a/xray-core/app/dns/dns_test.go
+++ b/xray-core/app/dns/dns_test.go
@@ -548,15 +548,8 @@ func TestIPMatch(t *testing.T) {
Port: uint32(port),
},
ExpectedIp: []*geodata.IPRule{
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- // inner ip, will not match
- Ip: []byte{192, 168, 11, 1},
- Prefix: 32,
- },
- },
- },
+ // inner ip, will not match
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}},
},
},
// second dns, match ip
@@ -571,22 +564,8 @@ func TestIPMatch(t *testing.T) {
Port: uint32(port),
},
ExpectedIp: []*geodata.IPRule{
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 8, 8},
- Prefix: 32,
- },
- },
- },
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 8, 4},
- Prefix: 32,
- },
- },
- },
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}},
},
},
},
@@ -676,9 +655,9 @@ func TestLocalDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will match localhost, localhost-a and localhost-b,
- {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}},
- {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}},
- {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}},
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}},
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}},
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}},
},
},
{
@@ -901,22 +880,8 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.8.8 and 8.8.4.4
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 8, 8},
- Prefix: 32,
- },
- },
- },
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 4, 4},
- Prefix: 32,
- },
- },
- },
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}},
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}},
},
},
{
@@ -936,14 +901,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will match 8.8.8.8 and 8.8.8.7, etc
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 8, 7},
- Prefix: 24,
- },
- },
- },
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}},
},
},
{
@@ -963,14 +921,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.7.7 (api.google.com)
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 7, 7},
- Prefix: 32,
- },
- },
- },
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}},
},
},
{
@@ -990,14 +941,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
},
ExpectedIp: []*geodata.IPRule{
// Will only match 8.8.7.8 (v2.api.google.com)
- {
- Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 7, 8},
- Prefix: 32,
- },
- },
- },
+ {Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}},
},
},
},
diff --git a/xray-core/app/router/command/command_test.go b/xray-core/app/router/command/command_test.go
index e6706df83d..adfe07dc61 100644
--- a/xray-core/app/router/command/command_test.go
+++ b/xray-core/app/router/command/command_test.go
@@ -308,7 +308,7 @@ func TestServiceTestRoute(t *testing.T) {
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
- SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
+ SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"},
},
{
diff --git a/xray-core/app/router/condition_test.go b/xray-core/app/router/condition_test.go
index 9e57aa914f..1b94bb8efe 100644
--- a/xray-core/app/router/condition_test.go
+++ b/xray-core/app/router/condition_test.go
@@ -92,25 +92,22 @@ func TestRoutingRule(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 8, 8},
- Prefix: 32,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
},
},
},
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{8, 8, 8, 8},
- Prefix: 32,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
},
},
},
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
- Prefix: 128,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
},
},
},
@@ -140,9 +137,8 @@ func TestRoutingRule(t *testing.T) {
SourceIp: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{192, 168, 0, 0},
- Prefix: 16,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
},
},
},
diff --git a/xray-core/app/router/router_test.go b/xray-core/app/router/router_test.go
index f038937fa1..323e9a6299 100644
--- a/xray-core/app/router/router_test.go
+++ b/xray-core/app/router/router_test.go
@@ -159,9 +159,8 @@ func TestIPOnDemand(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{192, 168, 0, 0},
- Prefix: 16,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
},
},
},
@@ -204,9 +203,8 @@ func TestIPIfNonMatchDomain(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{192, 168, 0, 0},
- Prefix: 16,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
},
},
},
@@ -249,9 +247,8 @@ func TestIPIfNonMatchIP(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{127, 0, 0, 0},
- Prefix: 8,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
},
},
},
diff --git a/xray-core/common/geodata/domain_matcher.go b/xray-core/common/geodata/domain_matcher.go
index a349061051..e5e854f9eb 100644
--- a/xray-core/common/geodata/domain_matcher.go
+++ b/xray-core/common/geodata/domain_matcher.go
@@ -11,7 +11,11 @@ import (
)
type DomainMatcher interface {
+ // Match returns the indices of all rules that match the input domain.
+ // The returned slice is owned by the caller and may be safely modified.
+ // Note: the slice may contain duplicates and the order is unspecified.
Match(input string) []uint32
+
MatchAny(input string) bool
}
diff --git a/xray-core/common/geodata/domain_matcher_test.go b/xray-core/common/geodata/domain_matcher_test.go
index 775b33719c..0506df094b 100644
--- a/xray-core/common/geodata/domain_matcher_test.go
+++ b/xray-core/common/geodata/domain_matcher_test.go
@@ -48,3 +48,25 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
+
+func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
+ matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
+ {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
+ {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
+ })
+ if err != nil {
+ t.Fatalf("BuildMatcher() failed: %v", err)
+ }
+
+ got := matcher.Match("example.com")
+ if !reflect.DeepEqual(got, []uint32{0, 1}) {
+ t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1})
+ }
+
+ got[0] = 1
+
+ gotAgain := matcher.Match("example.com")
+ if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) {
+ t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
+ }
+}
diff --git a/xray-core/common/geodata/geodat.pb.go b/xray-core/common/geodata/geodat.pb.go
index b5cc279738..369839c523 100644
--- a/xray-core/common/geodata/geodat.pb.go
+++ b/xray-core/common/geodata/geodat.pb.go
@@ -433,6 +433,58 @@ func (x *CIDR) GetPrefix() uint32 {
return 0
}
+type CIDRRule struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Cidr *CIDR `protobuf:"bytes,1,opt,name=cidr,proto3" json:"cidr,omitempty"`
+ ReverseMatch bool `protobuf:"varint,2,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CIDRRule) Reset() {
+ *x = CIDRRule{}
+ mi := &file_common_geodata_geodat_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CIDRRule) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CIDRRule) ProtoMessage() {}
+
+func (x *CIDRRule) ProtoReflect() protoreflect.Message {
+ mi := &file_common_geodata_geodat_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CIDRRule.ProtoReflect.Descriptor instead.
+func (*CIDRRule) Descriptor() ([]byte, []int) {
+ return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *CIDRRule) GetCidr() *CIDR {
+ if x != nil {
+ return x.Cidr
+ }
+ return nil
+}
+
+func (x *CIDRRule) GetReverseMatch() bool {
+ if x != nil {
+ return x.ReverseMatch
+ }
+ return false
+}
+
type GeoIP struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
@@ -444,7 +496,7 @@ type GeoIP struct {
func (x *GeoIP) Reset() {
*x = GeoIP{}
- mi := &file_common_geodata_geodat_proto_msgTypes[6]
+ mi := &file_common_geodata_geodat_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -456,7 +508,7 @@ func (x *GeoIP) String() string {
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
- mi := &file_common_geodata_geodat_proto_msgTypes[6]
+ mi := &file_common_geodata_geodat_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -469,7 +521,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
- return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
+ return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
}
func (x *GeoIP) GetCode() string {
@@ -502,7 +554,7 @@ type GeoIPList struct {
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
- mi := &file_common_geodata_geodat_proto_msgTypes[7]
+ mi := &file_common_geodata_geodat_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -514,7 +566,7 @@ func (x *GeoIPList) String() string {
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
- mi := &file_common_geodata_geodat_proto_msgTypes[7]
+ mi := &file_common_geodata_geodat_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -527,7 +579,7 @@ func (x *GeoIPList) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
- return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
+ return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
@@ -548,7 +600,7 @@ type GeoIPRule struct {
func (x *GeoIPRule) Reset() {
*x = GeoIPRule{}
- mi := &file_common_geodata_geodat_proto_msgTypes[8]
+ mi := &file_common_geodata_geodat_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -560,7 +612,7 @@ func (x *GeoIPRule) String() string {
func (*GeoIPRule) ProtoMessage() {}
func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
- mi := &file_common_geodata_geodat_proto_msgTypes[8]
+ mi := &file_common_geodata_geodat_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -573,7 +625,7 @@ func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
func (*GeoIPRule) Descriptor() ([]byte, []int) {
- return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
+ return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
}
func (x *GeoIPRule) GetFile() string {
@@ -610,7 +662,7 @@ type IPRule struct {
func (x *IPRule) Reset() {
*x = IPRule{}
- mi := &file_common_geodata_geodat_proto_msgTypes[9]
+ mi := &file_common_geodata_geodat_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -622,7 +674,7 @@ func (x *IPRule) String() string {
func (*IPRule) ProtoMessage() {}
func (x *IPRule) ProtoReflect() protoreflect.Message {
- mi := &file_common_geodata_geodat_proto_msgTypes[9]
+ mi := &file_common_geodata_geodat_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -635,7 +687,7 @@ func (x *IPRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
func (*IPRule) Descriptor() ([]byte, []int) {
- return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
+ return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10}
}
func (x *IPRule) GetValue() isIPRule_Value {
@@ -654,7 +706,7 @@ func (x *IPRule) GetGeoip() *GeoIPRule {
return nil
}
-func (x *IPRule) GetCustom() *CIDR {
+func (x *IPRule) GetCustom() *CIDRRule {
if x != nil {
if x, ok := x.Value.(*IPRule_Custom); ok {
return x.Custom
@@ -672,7 +724,7 @@ type IPRule_Geoip struct {
}
type IPRule_Custom struct {
- Custom *CIDR `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
+ Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
}
func (*IPRule_Geoip) isIPRule_Value() {}
@@ -693,7 +745,7 @@ type Domain_Attribute struct {
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
- mi := &file_common_geodata_geodat_proto_msgTypes[10]
+ mi := &file_common_geodata_geodat_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -705,7 +757,7 @@ func (x *Domain_Attribute) String() string {
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
- mi := &file_common_geodata_geodat_proto_msgTypes[10]
+ mi := &file_common_geodata_geodat_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -807,7 +859,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
"\x05value\".\n" +
"\x04CIDR\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
- "\x06prefix\x18\x02 \x01(\rR\x06prefix\"o\n" +
+ "\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" +
+ "\bCIDRRule\x12-\n" +
+ "\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
+ "\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" +
"\x05GeoIP\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
@@ -817,10 +872,10 @@ const file_common_geodata_geodat_proto_rawDesc = "" +
"\tGeoIPRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12#\n" +
- "\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"~\n" +
+ "\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" +
"\x06IPRule\x126\n" +
- "\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x123\n" +
- "\x06custom\x18\x02 \x01(\v2\x19.xray.common.geodata.CIDRH\x00R\x06customB\a\n" +
+ "\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" +
+ "\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" +
"\x05valueB[\n" +
"\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3"
@@ -837,7 +892,7 @@ func file_common_geodata_geodat_proto_rawDescGZIP() []byte {
}
var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_common_geodata_geodat_proto_goTypes = []any{
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
(*Domain)(nil), // 1: xray.common.geodata.Domain
@@ -846,28 +901,30 @@ var file_common_geodata_geodat_proto_goTypes = []any{
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
- (*GeoIP)(nil), // 7: xray.common.geodata.GeoIP
- (*GeoIPList)(nil), // 8: xray.common.geodata.GeoIPList
- (*GeoIPRule)(nil), // 9: xray.common.geodata.GeoIPRule
- (*IPRule)(nil), // 10: xray.common.geodata.IPRule
- (*Domain_Attribute)(nil), // 11: xray.common.geodata.Domain.Attribute
+ (*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule
+ (*GeoIP)(nil), // 8: xray.common.geodata.GeoIP
+ (*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList
+ (*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule
+ (*IPRule)(nil), // 11: xray.common.geodata.IPRule
+ (*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute
}
var file_common_geodata_geodat_proto_depIdxs = []int32{
0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type
- 11, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
+ 12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain
2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite
4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule
1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain
- 6, // 6: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
- 7, // 7: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
- 9, // 8: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
- 6, // 9: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDR
- 10, // [10:10] is the sub-list for method output_type
- 10, // [10:10] is the sub-list for method input_type
- 10, // [10:10] is the sub-list for extension type_name
- 10, // [10:10] is the sub-list for extension extendee
- 0, // [0:10] is the sub-list for field type_name
+ 6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR
+ 6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
+ 8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
+ 10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
+ 7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule
+ 11, // [11:11] is the sub-list for method output_type
+ 11, // [11:11] is the sub-list for method input_type
+ 11, // [11:11] is the sub-list for extension type_name
+ 11, // [11:11] is the sub-list for extension extendee
+ 0, // [0:11] is the sub-list for field type_name
}
func init() { file_common_geodata_geodat_proto_init() }
@@ -879,11 +936,11 @@ func file_common_geodata_geodat_proto_init() {
(*DomainRule_Geosite)(nil),
(*DomainRule_Custom)(nil),
}
- file_common_geodata_geodat_proto_msgTypes[9].OneofWrappers = []any{
+ file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
(*IPRule_Geoip)(nil),
(*IPRule_Custom)(nil),
}
- file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
+ file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
@@ -893,7 +950,7 @@ func file_common_geodata_geodat_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
NumEnums: 1,
- NumMessages: 11,
+ NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/xray-core/common/geodata/geodat.proto b/xray-core/common/geodata/geodat.proto
index 1828b91761..be91853e4e 100644
--- a/xray-core/common/geodata/geodat.proto
+++ b/xray-core/common/geodata/geodat.proto
@@ -66,6 +66,11 @@ message CIDR {
uint32 prefix = 2;
}
+message CIDRRule {
+ CIDR cidr = 1;
+ bool reverse_match = 2;
+}
+
message GeoIP {
string code = 1;
repeated CIDR cidr = 2;
@@ -85,6 +90,6 @@ message GeoIPRule {
message IPRule {
oneof value {
GeoIPRule geoip = 1;
- CIDR custom = 2;
+ CIDRRule custom = 2;
}
}
diff --git a/xray-core/common/geodata/ip_matcher.go b/xray-core/common/geodata/ip_matcher.go
index 565b38eab0..315ce040cf 100644
--- a/xray-core/common/geodata/ip_matcher.go
+++ b/xray-core/common/geodata/ip_matcher.go
@@ -940,45 +940,58 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) {
n := len(rules)
- custom := make([]*CIDR, 0, n)
- pos := make([]*GeoIPRule, 0, n)
- neg := make([]*GeoIPRule, 0, n)
+ posCustom := make([]*CIDR, 0, n)
+ negCustom := make([]*CIDR, 0, n)
+ posGeoip := make([]*GeoIPRule, 0, n)
+ negGeoip := make([]*GeoIPRule, 0, n)
for _, r := range rules {
switch v := r.Value.(type) {
case *IPRule_Custom:
- custom = append(custom, v.Custom)
+ if !v.Custom.ReverseMatch {
+ posCustom = append(posCustom, v.Custom.Cidr)
+ } else {
+ negCustom = append(negCustom, v.Custom.Cidr)
+ }
case *IPRule_Geoip:
if !v.Geoip.ReverseMatch {
- pos = append(pos, v.Geoip)
+ posGeoip = append(posGeoip, v.Geoip)
} else {
- neg = append(neg, v.Geoip)
+ negGeoip = append(negGeoip, v.Geoip)
}
default:
panic("unknown ip rule type")
}
}
- subs := make([]*HeuristicIPMatcher, 0, 3)
+ subs := make([]*HeuristicIPMatcher, 0, 4)
- if len(custom) > 0 {
- ipset, err := f.CreateFromCIDRs(custom)
+ if len(posCustom) > 0 {
+ ipset, err := f.CreateFromCIDRs(posCustom)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
}
- if len(pos) > 0 {
- ipset, err := f.GetOrCreateFromGeoIPRules(pos)
+ if len(negCustom) > 0 {
+ ipset, err := f.CreateFromCIDRs(negCustom)
+ if err != nil {
+ return nil, err
+ }
+ subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
+ }
+
+ if len(posGeoip) > 0 {
+ ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
}
- if len(neg) > 0 {
- ipset, err := f.GetOrCreateFromGeoIPRules(neg)
+ if len(negGeoip) > 0 {
+ ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
if err != nil {
return nil, err
}
diff --git a/xray-core/common/geodata/ip_matcher_test.go b/xray-core/common/geodata/ip_matcher_test.go
index ac627506e2..d1dc2de228 100644
--- a/xray-core/common/geodata/ip_matcher_test.go
+++ b/xray-core/common/geodata/ip_matcher_test.go
@@ -189,6 +189,34 @@ func TestIPReverseMatcher2(t *testing.T) {
}
}
+func TestIPCustomReverseMatcher(t *testing.T) {
+ matcher := buildIPMatcher("!8.8.8.8/32")
+
+ testCases := []struct {
+ Input string
+ Output bool
+ }{
+ {
+ Input: "8.8.8.8",
+ Output: false,
+ },
+ {
+ Input: "1.1.1.1",
+ Output: true,
+ },
+ {
+ Input: "2001:cdba::3257:9652",
+ Output: false,
+ },
+ }
+
+ for _, test := range testCases {
+ if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
+ t.Error("unexpected output: ", v, " for test case ", test)
+ }
+ }
+}
+
func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
diff --git a/xray-core/common/geodata/rule_parser.go b/xray-core/common/geodata/rule_parser.go
index 1184f553d9..c05fed31e6 100644
--- a/xray-core/common/geodata/rule_parser.go
+++ b/xray-core/common/geodata/rule_parser.go
@@ -17,6 +17,8 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
var ipRules []*IPRule
for i, r := range rules {
+ r, reverse := cutReversePrefix(r)
+
if strings.HasPrefix(r, "geoip:") {
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
}
@@ -32,9 +34,9 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
var rule isIPRule_Value
var err error
if prefix > 0 {
- rule, err = parseGeoIPRule(r[prefix:])
+ rule, err = parseGeoIPRule(r[prefix:], reverse)
} else {
- rule, err = parseCustomIPRule(r)
+ rule, err = parseCustomIPRule(r, reverse)
}
if err != nil {
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
@@ -45,7 +47,16 @@ func ParseIPRules(rules []string) ([]*IPRule, error) {
return ipRules, nil
}
-func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
+func cutReversePrefix(s string) (string, bool) {
+ reverse := false
+ for strings.HasPrefix(s, "!") {
+ s = s[1:]
+ reverse = !reverse
+ }
+ return s, reverse
+}
+
+func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
file, code, ok := strings.Cut(rule, ":")
if !ok {
return nil, errors.New("syntax error")
@@ -55,11 +66,8 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
return nil, errors.New("empty file")
}
- reverse := false
- if strings.HasPrefix(code, "!") {
- code = code[1:]
- reverse = true
- }
+ code, codeReverse := cutReversePrefix(code)
+ reverse = reverse != codeReverse
if code == "" {
return nil, errors.New("empty code")
}
@@ -78,13 +86,16 @@ func parseGeoIPRule(rule string) (*IPRule_Geoip, error) {
}, nil
}
-func parseCustomIPRule(rule string) (*IPRule_Custom, error) {
+func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
cidr, err := parseCIDR(rule)
if err != nil {
return nil, err
}
return &IPRule_Custom{
- Custom: cidr,
+ Custom: &CIDRRule{
+ Cidr: cidr,
+ ReverseMatch: reverse,
+ },
}, nil
}
diff --git a/xray-core/common/geodata/rule_parser_test.go b/xray-core/common/geodata/rule_parser_test.go
index 87dbaaab92..7aaf44c5cf 100644
--- a/xray-core/common/geodata/rule_parser_test.go
+++ b/xray-core/common/geodata/rule_parser_test.go
@@ -13,12 +13,20 @@ func TestParseIPRules(t *testing.T) {
rules := []string{
"geoip:us",
"geoip:cn",
+ "!geoip:cn",
+ "!!geoip:cn",
"geoip:!cn",
+ "geoip:!!cn",
+ "!geoip:!cn",
"ext:geoip.dat:!cn",
+ "ext:geoip.dat:!!cn",
"ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ca",
"192.168.0.0/24",
+ "!192.168.0.0/24",
+ "!!192.168.0.0/24",
+ "!!!192.168.0.0/24",
"192.168.0.1",
"fe80::/64",
"fe80::",
@@ -30,6 +38,53 @@ func TestParseIPRules(t *testing.T) {
}
}
+func TestParseIPRuleReverse(t *testing.T) {
+ t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
+
+ for _, tt := range []struct {
+ rule string
+ reverse bool
+ }{
+ {rule: "!192.168.0.0/24", reverse: true},
+ {rule: "!!192.168.0.0/24", reverse: false},
+ {rule: "!!!192.168.0.0/24", reverse: true},
+ {rule: "!!!!192.168.0.0/24", reverse: false},
+ {rule: "geoip:cn", reverse: false},
+ {rule: "!geoip:cn", reverse: true},
+ {rule: "!!geoip:cn", reverse: false},
+ {rule: "geoip:!cn", reverse: true},
+ {rule: "geoip:!!cn", reverse: false},
+ {rule: "!geoip:!cn", reverse: false},
+ {rule: "!!geoip:!cn", reverse: true},
+ {rule: "!geoip:!!cn", reverse: true},
+ {rule: "ext:geoip.dat:!!!cn", reverse: true},
+ } {
+ t.Run(tt.rule, func(t *testing.T) {
+ rules, err := geodata.ParseIPRules([]string{tt.rule})
+ if err != nil {
+ t.Fatalf("Failed to parse ip rules, got %s", err)
+ }
+
+ if len(rules) != 1 {
+ t.Fatalf("Expected 1 rule, got %d", len(rules))
+ }
+
+ switch rule := rules[0]; {
+ case rule.GetGeoip() != nil:
+ if rule.GetGeoip().GetReverseMatch() != tt.reverse {
+ t.Fatalf("Expected geoip reverse match to be %t", tt.reverse)
+ }
+ case rule.GetCustom() != nil:
+ if rule.GetCustom().GetReverseMatch() != tt.reverse {
+ t.Fatalf("Expected custom reverse match to be %t", tt.reverse)
+ }
+ default:
+ t.Fatal("Expected ip rule")
+ }
+ })
+ }
+}
+
func TestParseDomainRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
diff --git a/xray-core/common/geodata/strmatcher/matchers.go b/xray-core/common/geodata/strmatcher/matchers.go
index 7e073764c8..fa28880461 100644
--- a/xray-core/common/geodata/strmatcher/matchers.go
+++ b/xray-core/common/geodata/strmatcher/matchers.go
@@ -3,6 +3,7 @@ package strmatcher
import (
"errors"
"regexp"
+ "slices"
"strings"
"unicode/utf8"
@@ -253,13 +254,12 @@ func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error {
}
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
-// It is designed to avoid new memory allocation as possible.
func CompositeMatches(matches [][]uint32) []uint32 {
switch len(matches) {
case 0:
return nil
case 1:
- return matches[0]
+ return slices.Clone(matches[0])
default:
result := make([]uint32, 0, 5)
for i := 0; i < len(matches); i++ {
diff --git a/xray-core/common/geodata/strmatcher/strmatcher.go b/xray-core/common/geodata/strmatcher/strmatcher.go
index e4187f6330..2cc6f252fc 100644
--- a/xray-core/common/geodata/strmatcher/strmatcher.go
+++ b/xray-core/common/geodata/strmatcher/strmatcher.go
@@ -62,6 +62,7 @@ type IndexMatcher interface {
// Match returns the indices of all matchers that matches the input.
// * Empty array is returned if no such matcher exists.
// * The order of returned matchers should follow priority specification.
+ // * The returned slice is owned by the caller and may be safely modified.
// Priority specification:
// 1. Priority between matcher types: full > domain > substr > regex.
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
@@ -89,6 +90,7 @@ type ValueMatcher interface {
// * Empty array is returned if no such matcher exists.
// * The order of returned values should follow priority specification.
// * Same value may appear multiple times if multiple matched matchers were added with that value.
+ // * The returned slice is owned by the caller and may be safely modified.
// Priority specification:
// 1. Priority between matcher types: full > domain > substr > regex.
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
diff --git a/xray-core/core/core.go b/xray-core/core/core.go
index ccd1437d7d..2ba9b9cb96 100644
--- a/xray-core/core/core.go
+++ b/xray-core/core/core.go
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 4
- Version_z byte = 15
+ Version_z byte = 17
)
var (
diff --git a/xray-core/go.mod b/xray-core/go.mod
index 7ea49526f2..b079fba3c1 100644
--- a/xray-core/go.mod
+++ b/xray-core/go.mod
@@ -12,7 +12,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
- github.com/pires/go-proxyproto v0.11.0
+ github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
diff --git a/xray-core/go.sum b/xray-core/go.sum
index 4ae1f959a6..f448783c82 100644
--- a/xray-core/go.sum
+++ b/xray-core/go.sum
@@ -45,8 +45,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
-github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
-github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
+github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
+github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
diff --git a/xray-core/infra/conf/router_test.go b/xray-core/infra/conf/router_test.go
index 26ebff1c96..130cf4f785 100644
--- a/xray-core/infra/conf/router_test.go
+++ b/xray-core/infra/conf/router_test.go
@@ -135,17 +135,15 @@ func TestRouterConfig(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{10, 0, 0, 0},
- Prefix: 8,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
},
},
},
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
- Prefix: 128,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
},
},
},
@@ -216,17 +214,15 @@ func TestRouterConfig(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{10, 0, 0, 0},
- Prefix: 8,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
},
},
},
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
- Prefix: 128,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128},
},
},
},
diff --git a/xray-core/infra/conf/xray_test.go b/xray-core/infra/conf/xray_test.go
index d415f937a1..d4ff7126ae 100644
--- a/xray-core/infra/conf/xray_test.go
+++ b/xray-core/infra/conf/xray_test.go
@@ -99,9 +99,8 @@ func TestXrayConfig(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{10, 0, 0, 0},
- Prefix: 8,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
},
},
},
@@ -216,8 +215,12 @@ func TestSniffingConfig_Build(t *testing.T) {
if rule == nil {
t.Fatalf("SniffingConfig.Build() produced a non-custom ip rule at index %d", i)
}
- if !reflect.DeepEqual(rule.Ip, tc.ip) || rule.Prefix != tc.prefix {
- t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, rule.Ip, rule.Prefix, tc.ip, tc.prefix)
+ cidr := rule.GetCidr()
+ if cidr == nil {
+ t.Fatalf("SniffingConfig.Build() produced a custom ip rule without cidr at index %d", i)
+ }
+ if !reflect.DeepEqual(cidr.Ip, tc.ip) || cidr.Prefix != tc.prefix {
+ t.Fatalf("SniffingConfig.Build() produced wrong ip rule at index %d: got (%v, %d), want (%v, %d)", i, cidr.Ip, cidr.Prefix, tc.ip, tc.prefix)
}
}
}
diff --git a/xray-core/proxy/freedom/freedom.go b/xray-core/proxy/freedom/freedom.go
index 008f700fe7..bc05d04a40 100644
--- a/xray-core/proxy/freedom/freedom.go
+++ b/xray-core/proxy/freedom/freedom.go
@@ -290,7 +290,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
if destination.Network == net.Network_TCP {
reader = buf.NewReader(conn)
} else {
- reader = NewPacketReader(conn, UDPOverride, destination)
+ reader = NewPacketReader(conn, UDPOverride, destination, blockedIPMatcher)
}
if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to process response").Base(err)
@@ -309,7 +309,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return nil
}
-func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {
+func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination, blockedIPMatcher geodata.IPMatcher) buf.Reader {
iConn := conn
statConn, ok := iConn.(*stat.CounterConnection)
if ok {
@@ -328,6 +328,7 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
return &PacketReader{
PacketConnWrapper: c,
Counter: counter,
+ BlockedIPMatcher: blockedIPMatcher,
IsOverridden: isOverridden,
InitUnchangedAddr: DialDest.Address,
InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address,
@@ -339,6 +340,7 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
type PacketReader struct {
*internet.PacketConnWrapper
stats.Counter
+ BlockedIPMatcher geodata.IPMatcher
IsOverridden bool
InitUnchangedAddr net.Address
InitChangedAddr net.Address
@@ -346,30 +348,38 @@ type PacketReader struct {
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
b := buf.New()
- b.Resize(0, buf.Size)
- n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
- if err != nil {
- b.Release()
- return nil, err
- }
- b.Resize(0, int32(n))
- // if udp dest addr is changed, we are unable to get the correct src addr
- // so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
- if !r.IsOverridden {
- address := net.IPAddress(d.(*net.UDPAddr).IP)
- if r.InitChangedAddr == address {
- address = r.InitUnchangedAddr
+ for {
+ b.Resize(0, buf.Size)
+ n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes())
+ if err != nil {
+ b.Release()
+ return nil, err
}
- b.UDP = &net.Destination{
- Address: address,
- Port: net.Port(d.(*net.UDPAddr).Port),
- Network: net.Network_UDP,
+ b.Resize(0, int32(n))
+
+ udpAddr := d.(*net.UDPAddr)
+ sourceAddr := net.IPAddress(udpAddr.IP)
+ if isBlockedAddress(r.BlockedIPMatcher, sourceAddr) {
+ continue
}
+
+ // if udp dest addr is changed, we are unable to get the correct src addr
+ // so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
+ if !r.IsOverridden {
+ if r.InitChangedAddr == sourceAddr {
+ sourceAddr = r.InitUnchangedAddr
+ }
+ b.UDP = &net.Destination{
+ Address: sourceAddr,
+ Port: net.Port(udpAddr.Port),
+ Network: net.Network_UDP,
+ }
+ }
+ if r.Counter != nil {
+ r.Counter.Add(int64(n))
+ }
+ return buf.MultiBuffer{b}, nil
}
- if r.Counter != nil {
- r.Counter.Add(int64(n))
- }
- return buf.MultiBuffer{b}, nil
}
// DialDest means the dial target used in the dialer when creating conn
@@ -468,10 +478,8 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
}
}
if isBlockedAddress(w.BlockedIPMatcher, b.UDP.Address) {
- blockedAddr := b.UDP.Address
b.Release()
- buf.ReleaseMulti(mb)
- return errors.New("blocked target IP: ", blockedAddr).AtDebug()
+ continue
}
destAddr := b.UDP.RawNetAddr()
if destAddr == nil {
diff --git a/xray-core/proxy/loopback/loopback.go b/xray-core/proxy/loopback/loopback.go
index ab3360ae49..2f54fe7476 100644
--- a/xray-core/proxy/loopback/loopback.go
+++ b/xray-core/proxy/loopback/loopback.go
@@ -45,9 +45,11 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet
ctx = session.ContextWithContent(ctx, content)
- inbound := session.InboundFromContext(ctx)
- if inbound == nil {
- inbound = &session.Inbound{}
+ inbound := &session.Inbound{}
+ originInbound := session.InboundFromContext(ctx)
+ if originInbound != nil {
+ // get a shallow copy to avoid modifying the inbound tag in upstream context
+ *inbound = *originInbound
}
inbound.Tag = l.config.InboundTag
diff --git a/xray-core/testing/scenarios/dns_test.go b/xray-core/testing/scenarios/dns_test.go
index 4f7fad5d94..9be19a7d8c 100644
--- a/xray-core/testing/scenarios/dns_test.go
+++ b/xray-core/testing/scenarios/dns_test.go
@@ -47,9 +47,8 @@ func TestResolveIP(t *testing.T) {
Ip: []*geodata.IPRule{
{
Value: &geodata.IPRule_Custom{
- Custom: &geodata.CIDR{
- Ip: []byte{127, 0, 0, 0},
- Prefix: 8,
+ Custom: &geodata.CIDRRule{
+ Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
},
},
},
diff --git a/xray-core/transport/internet/finalmask/header/custom/evaluator.go b/xray-core/transport/internet/finalmask/header/custom/evaluator.go
index 4665570060..728be54384 100644
--- a/xray-core/transport/internet/finalmask/header/custom/evaluator.go
+++ b/xray-core/transport/internet/finalmask/header/custom/evaluator.go
@@ -9,8 +9,9 @@ import (
)
type evalValue struct {
- bytes []byte
- u64 *uint64
+ bytes []byte
+ u64 *uint64
+ isBytes bool
}
type evalContext struct {
@@ -175,7 +176,7 @@ func evaluateExpr(expr *Expr, ctx *evalContext) (evalValue, error) {
}
out = append(out, bytesValue...)
}
- return evalValue{bytes: out}, nil
+ return evalValue{bytes: out, isBytes: true}, nil
case "slice":
if len(expr.GetArgs()) != 3 {
return evalValue{}, errors.New("slice expects 3 args")
@@ -208,52 +209,236 @@ func evaluateExpr(expr *Expr, ctx *evalContext) (evalValue, error) {
if end > uint64(len(sourceBytes)) {
return evalValue{}, errors.New("slice out of bounds")
}
- return evalValue{bytes: append([]byte(nil), sourceBytes[offsetU64:end]...)}, nil
+ return evalValue{bytes: append([]byte(nil), sourceBytes[offsetU64:end]...), isBytes: true}, nil
case "xor16":
return evaluateXor(expr.GetArgs(), 0xFFFF, 2, ctx)
case "xor32":
return evaluateXor(expr.GetArgs(), 0xFFFFFFFF, 4, ctx)
case "be16":
- if len(expr.GetArgs()) != 1 {
- return evalValue{}, errors.New("be16 expects 1 arg")
- }
- value, err := evaluateExprArg(expr.GetArgs()[0], ctx)
- if err != nil {
- return evalValue{}, err
- }
- u64Value, err := value.asU64()
- if err != nil {
- return evalValue{}, err
- }
- if u64Value > 0xFFFF {
- return evalValue{}, errors.New("be16 overflow")
- }
- out := make([]byte, 2)
- binary.BigEndian.PutUint16(out, uint16(u64Value))
- return evalValue{bytes: out}, nil
+ return evaluatePack(expr.GetArgs(), "be16", 2, binary.BigEndian, ctx)
case "be32":
- if len(expr.GetArgs()) != 1 {
- return evalValue{}, errors.New("be32 expects 1 arg")
- }
- value, err := evaluateExprArg(expr.GetArgs()[0], ctx)
- if err != nil {
- return evalValue{}, err
- }
- u64Value, err := value.asU64()
- if err != nil {
- return evalValue{}, err
- }
- if u64Value > 0xFFFFFFFF {
- return evalValue{}, errors.New("be32 overflow")
- }
- out := make([]byte, 4)
- binary.BigEndian.PutUint32(out, uint32(u64Value))
- return evalValue{bytes: out}, nil
+ return evaluatePack(expr.GetArgs(), "be32", 4, binary.BigEndian, ctx)
+ case "le16":
+ return evaluatePack(expr.GetArgs(), "le16", 2, binary.LittleEndian, ctx)
+ case "le32":
+ return evaluatePack(expr.GetArgs(), "le32", 4, binary.LittleEndian, ctx)
+ case "le64":
+ return evaluatePack(expr.GetArgs(), "le64", 8, binary.LittleEndian, ctx)
+ case "pad":
+ return evaluatePad(expr.GetArgs(), ctx)
+ case "truncate":
+ return evaluateTruncate(expr.GetArgs(), ctx)
+ case "add":
+ return evaluateBinaryU64Op(expr.GetArgs(), "add", ctx, func(left, right uint64) (uint64, error) {
+ if left > ^uint64(0)-right {
+ return 0, errors.New("add overflow")
+ }
+ return left + right, nil
+ })
+ case "sub":
+ return evaluateBinaryU64Op(expr.GetArgs(), "sub", ctx, func(left, right uint64) (uint64, error) {
+ if left < right {
+ return 0, errors.New("sub underflow")
+ }
+ return left - right, nil
+ })
+ case "and":
+ return evaluateBinaryU64Op(expr.GetArgs(), "and", ctx, func(left, right uint64) (uint64, error) {
+ return left & right, nil
+ })
+ case "or":
+ return evaluateBinaryU64Op(expr.GetArgs(), "or", ctx, func(left, right uint64) (uint64, error) {
+ return left | right, nil
+ })
+ case "shl":
+ return evaluateShift(expr.GetArgs(), "shl", ctx, func(value uint64, shift uint) (uint64, error) {
+ if shift >= 64 {
+ return 0, errors.New("shift out of range")
+ }
+ if value > (^uint64(0) >> shift) {
+ return 0, errors.New("shl overflow")
+ }
+ return value << shift, nil
+ })
+ case "shr":
+ return evaluateShift(expr.GetArgs(), "shr", ctx, func(value uint64, shift uint) (uint64, error) {
+ if shift >= 64 {
+ return 0, errors.New("shift out of range")
+ }
+ return value >> shift, nil
+ })
default:
return evalValue{}, errors.New("unsupported expr op: ", expr.GetOp())
}
}
+func evaluatePack(args []*ExprArg, name string, width int, order binary.ByteOrder, ctx *evalContext) (evalValue, error) {
+ if len(args) != 1 {
+ return evalValue{}, errors.New(name, " expects 1 arg")
+ }
+ value, err := evaluateExprArg(args[0], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ u64Value, err := value.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+
+ switch width {
+ case 2:
+ if u64Value > 0xFFFF {
+ return evalValue{}, errors.New(name, " overflow")
+ }
+ out := make([]byte, 2)
+ order.PutUint16(out, uint16(u64Value))
+ return evalValue{bytes: out, isBytes: true}, nil
+ case 4:
+ if u64Value > 0xFFFFFFFF {
+ return evalValue{}, errors.New(name, " overflow")
+ }
+ out := make([]byte, 4)
+ order.PutUint32(out, uint32(u64Value))
+ return evalValue{bytes: out, isBytes: true}, nil
+ case 8:
+ out := make([]byte, 8)
+ order.PutUint64(out, u64Value)
+ return evalValue{bytes: out, isBytes: true}, nil
+ default:
+ return evalValue{}, errors.New("unsupported pack width")
+ }
+}
+
+func evaluatePad(args []*ExprArg, ctx *evalContext) (evalValue, error) {
+ if len(args) != 3 {
+ return evalValue{}, errors.New("pad expects 3 args")
+ }
+ source, err := evaluateExprArg(args[0], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ target, err := evaluateExprArg(args[1], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ fill, err := evaluateExprArg(args[2], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ sourceBytes, err := source.asBytes()
+ if err != nil {
+ return evalValue{}, err
+ }
+ targetU64, err := target.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+ fillBytes, err := fill.asBytes()
+ if err != nil {
+ return evalValue{}, err
+ }
+ if len(fillBytes) == 0 {
+ return evalValue{}, errors.New("pad fill must not be empty")
+ }
+ if targetU64 < uint64(len(sourceBytes)) {
+ return evalValue{}, errors.New("pad target shorter than source")
+ }
+
+ out := append([]byte(nil), sourceBytes...)
+ for uint64(len(out)) < targetU64 {
+ remaining := int(targetU64) - len(out)
+ if remaining >= len(fillBytes) {
+ out = append(out, fillBytes...)
+ continue
+ }
+ out = append(out, fillBytes[:remaining]...)
+ }
+ return evalValue{bytes: out, isBytes: true}, nil
+}
+
+func evaluateTruncate(args []*ExprArg, ctx *evalContext) (evalValue, error) {
+ if len(args) != 2 {
+ return evalValue{}, errors.New("truncate expects 2 args")
+ }
+ source, err := evaluateExprArg(args[0], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ length, err := evaluateExprArg(args[1], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ sourceBytes, err := source.asBytes()
+ if err != nil {
+ return evalValue{}, err
+ }
+ lengthU64, err := length.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+ if lengthU64 > uint64(len(sourceBytes)) {
+ return evalValue{}, errors.New("truncate out of bounds")
+ }
+ return evalValue{bytes: append([]byte(nil), sourceBytes[:lengthU64]...), isBytes: true}, nil
+}
+
+func evaluateBinaryU64Op(args []*ExprArg, name string, ctx *evalContext, op func(left, right uint64) (uint64, error)) (evalValue, error) {
+ if len(args) != 2 {
+ return evalValue{}, errors.New(name, " expects 2 args")
+ }
+ left, err := evaluateExprArg(args[0], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ right, err := evaluateExprArg(args[1], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ leftU64, err := left.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+ rightU64, err := right.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+ result, err := op(leftU64, rightU64)
+ if err != nil {
+ return evalValue{}, err
+ }
+ return evalValue{u64: &result}, nil
+}
+
+func evaluateShift(args []*ExprArg, name string, ctx *evalContext, op func(value uint64, shift uint) (uint64, error)) (evalValue, error) {
+ if len(args) != 2 {
+ return evalValue{}, errors.New(name, " expects 2 args")
+ }
+ value, err := evaluateExprArg(args[0], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ shift, err := evaluateExprArg(args[1], ctx)
+ if err != nil {
+ return evalValue{}, err
+ }
+ valueU64, err := value.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+ shiftU64, err := shift.asU64()
+ if err != nil {
+ return evalValue{}, err
+ }
+ if shiftU64 >= 64 {
+ return evalValue{}, errors.New("shift out of range")
+ }
+ result, err := op(valueU64, uint(shiftU64))
+ if err != nil {
+ return evalValue{}, err
+ }
+ return evalValue{u64: &result}, nil
+}
+
func evaluateXor(args []*ExprArg, mask uint64, width int, ctx *evalContext) (evalValue, error) {
if len(args) != 2 {
return evalValue{}, errors.New("xor expects 2 args")
@@ -309,6 +494,30 @@ func measureExpr(expr *Expr, sizeCtx map[string]int) (int, error) {
return 2, nil
case "be32":
return 4, nil
+ case "le16":
+ return 2, nil
+ case "le32":
+ return 4, nil
+ case "le64":
+ return 8, nil
+ case "pad":
+ if len(expr.GetArgs()) != 3 {
+ return 0, errors.New("pad expects 3 args")
+ }
+ lengthArg := expr.GetArgs()[1]
+ if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok {
+ return int(value.U64), nil
+ }
+ return 0, errors.New("pad length must be u64")
+ case "truncate":
+ if len(expr.GetArgs()) != 2 {
+ return 0, errors.New("truncate expects 2 args")
+ }
+ lengthArg := expr.GetArgs()[1]
+ if value, ok := lengthArg.GetValue().(*ExprArg_U64); ok {
+ return int(value.U64), nil
+ }
+ return 0, errors.New("truncate length must be u64")
default:
return 0, errors.New("expr size is not bytes for op: ", expr.GetOp())
}
@@ -317,7 +526,7 @@ func measureExpr(expr *Expr, sizeCtx map[string]int) (int, error) {
func evaluateExprArg(arg *ExprArg, ctx *evalContext) (evalValue, error) {
switch value := arg.GetValue().(type) {
case *ExprArg_Bytes:
- return evalValue{bytes: append([]byte(nil), value.Bytes...)}, nil
+ return evalValue{bytes: append([]byte(nil), value.Bytes...), isBytes: true}, nil
case *ExprArg_U64:
return evalValue{u64: &value.U64}, nil
case *ExprArg_Var:
@@ -325,7 +534,7 @@ func evaluateExprArg(arg *ExprArg, ctx *evalContext) (evalValue, error) {
if !ok {
return evalValue{}, errors.New("unknown variable: ", value.Var)
}
- return evalValue{bytes: append([]byte(nil), saved...)}, nil
+ return evalValue{bytes: append([]byte(nil), saved...), isBytes: true}, nil
case *ExprArg_Metadata:
metadata, ok := ctx.metadata[value.Metadata]
if !ok {
@@ -361,7 +570,7 @@ func measureExprArg(arg *ExprArg, sizeCtx map[string]int) (int, error) {
}
func (v evalValue) asBytes() ([]byte, error) {
- if v.bytes != nil {
+ if v.isBytes {
return append([]byte(nil), v.bytes...), nil
}
return nil, errors.New("expr value is not bytes")
diff --git a/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go b/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go
index 6bd342641e..51a9d2771e 100644
--- a/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go
+++ b/xray-core/transport/internet/finalmask/header/custom/evaluator_test.go
@@ -128,3 +128,364 @@ func TestEvaluatorRejectsInvalidArgType(t *testing.T) {
t.Fatal("expected evaluator error")
}
}
+
+func TestEvaluatorLittleEndianProducesExpectedBytes(t *testing.T) {
+ items := []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "concat",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "le16",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 0x1234}},
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "le32",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 0xA1B2C3D4}},
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "le64",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 0x0102030405060708}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ got, err := evaluateUDPItems(items)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := []byte{
+ 0x34, 0x12,
+ 0xD4, 0xC3, 0xB2, 0xA1,
+ 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
+ }
+ if !bytes.Equal(got, want) {
+ t.Fatalf("unexpected output: %x", got)
+ }
+}
+
+func TestEvaluatorPadAndTruncateShapeBytes(t *testing.T) {
+ items := []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "concat",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "pad",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{0xAA, 0xBB}}},
+ {Value: &ExprArg_U64{U64: 5}},
+ {Value: &ExprArg_Bytes{Bytes: []byte{0xCC, 0xDD}}},
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "truncate",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}},
+ {Value: &ExprArg_U64{U64: 2}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ got, err := evaluateUDPItems(items)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xCC, 0x01, 0x02}
+ if !bytes.Equal(got, want) {
+ t.Fatalf("unexpected output: %x", got)
+ }
+}
+
+func TestMeasureUDPItemsSupportsPadAndTruncate(t *testing.T) {
+ items := []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "pad",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}},
+ {Value: &ExprArg_U64{U64: 4}},
+ {Value: &ExprArg_Bytes{Bytes: []byte{0x00}}},
+ },
+ },
+ },
+ {
+ Expr: &Expr{
+ Op: "truncate",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}},
+ {Value: &ExprArg_U64{U64: 3}},
+ },
+ },
+ },
+ }
+
+ got, err := measureUDPItems(items)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if got != 7 {
+ t.Fatalf("unexpected size: %d", got)
+ }
+}
+
+func TestEvaluatorArithmeticAndBitwiseProduceExpectedBytes(t *testing.T) {
+ items := []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "concat",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "add",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 1}},
+ {Value: &ExprArg_U64{U64: 2}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "sub",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 10}},
+ {Value: &ExprArg_U64{U64: 3}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "and",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 0xF0F0}},
+ {Value: &ExprArg_U64{U64: 0x0FF0}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "or",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "shl",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 1}},
+ {Value: &ExprArg_U64{U64: 8}},
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "shr",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 0x80}},
+ {Value: &ExprArg_U64{U64: 7}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ got, err := evaluateUDPItems(items)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := []byte{
+ 0x00, 0x03,
+ 0x00, 0x07,
+ 0x00, 0xF0,
+ 0x01, 0x01,
+ }
+ if !bytes.Equal(got, want) {
+ t.Fatalf("unexpected output: %x", got)
+ }
+}
+
+func TestEvaluatorRejectsInvalidShapingAndArithmetic(t *testing.T) {
+ tests := []struct {
+ name string
+ items []*UDPItem
+ match string
+ }{
+ {
+ name: "pad with empty fill",
+ items: []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "pad",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}},
+ {Value: &ExprArg_U64{U64: 4}},
+ {Value: &ExprArg_Bytes{Bytes: []byte{}}},
+ },
+ },
+ },
+ },
+ match: "pad fill",
+ },
+ {
+ name: "truncate beyond source",
+ items: []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "truncate",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{1, 2}}},
+ {Value: &ExprArg_U64{U64: 3}},
+ },
+ },
+ },
+ },
+ match: "truncate",
+ },
+ {
+ name: "sub underflow",
+ items: []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "sub",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 1}},
+ {Value: &ExprArg_U64{U64: 2}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ match: "underflow",
+ },
+ {
+ name: "shift too large",
+ items: []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "shl",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 1}},
+ {Value: &ExprArg_U64{U64: 64}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ match: "shift",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := evaluateUDPItems(tt.items)
+ if err == nil {
+ t.Fatal("expected evaluator error")
+ }
+ if !bytes.Contains([]byte(err.Error()), []byte(tt.match)) {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+ }
+}
diff --git a/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go b/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go
index c66d4c41de..e0a091f4df 100644
--- a/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go
+++ b/xray-core/transport/internet/finalmask/header/custom/udp_runtime_test.go
@@ -1,6 +1,10 @@
package custom
-import "testing"
+import (
+ "bytes"
+ "net"
+ "testing"
+)
func TestDSLUDPClientSizeTracksEvaluatedItems(t *testing.T) {
conn, err := NewConnClientUDP(&UDPConfig{
@@ -81,3 +85,104 @@ func TestDSLUDPServerRejectsMalformedVarReference(t *testing.T) {
t.Fatal("expected packet mismatch")
}
}
+
+func TestDSLUDPClientWriteSupportsExtendedExprOps(t *testing.T) {
+ conn, err := NewConnClientUDP(&UDPConfig{
+ Client: []*UDPItem{
+ {
+ Expr: &Expr{
+ Op: "le16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "add",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 1}},
+ {Value: &ExprArg_U64{U64: 2}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Expr: &Expr{
+ Op: "pad",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}},
+ {Value: &ExprArg_U64{U64: 3}},
+ {Value: &ExprArg_Bytes{Bytes: []byte{0xBB}}},
+ },
+ },
+ },
+ {
+ Expr: &Expr{
+ Op: "truncate",
+ Args: []*ExprArg{
+ {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}},
+ {Value: &ExprArg_U64{U64: 2}},
+ },
+ },
+ },
+ {
+ Expr: &Expr{
+ Op: "be16",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "or",
+ Args: []*ExprArg{
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "shl",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 1}},
+ {Value: &ExprArg_U64{U64: 8}},
+ },
+ },
+ },
+ },
+ {
+ Value: &ExprArg_Expr{
+ Expr: &Expr{
+ Op: "shr",
+ Args: []*ExprArg{
+ {Value: &ExprArg_U64{U64: 0x80}},
+ {Value: &ExprArg_U64{U64: 7}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client := conn.(*udpCustomClientConn)
+ buf := make([]byte, client.Size())
+ if _, err := client.WriteTo(buf, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53}); err != nil {
+ t.Fatal(err)
+ }
+
+ want := []byte{
+ 0x03, 0x00,
+ 0xAA, 0xBB, 0xBB,
+ 0x01, 0x02,
+ 0x01, 0x01,
+ }
+ if !bytes.Equal(buf, want) {
+ t.Fatalf("unexpected encoded header: %x", buf)
+ }
+}