mirror of
				https://github.com/kenzok8/openwrt-packages.git
				synced 2025-11-03 08:28:50 +08:00 
			
		
		
		
	update 2024-08-11 23:35:24
This commit is contained in:
		@ -1,28 +0,0 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 | 
			
		||||
include $(TOPDIR)/rules.mk
 | 
			
		||||
 | 
			
		||||
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64
 | 
			
		||||
LUCI_PKGARCH:=all
 | 
			
		||||
LUCI_DEPENDS:= \
 | 
			
		||||
	+sing-box \
 | 
			
		||||
	+chinadns-ng \
 | 
			
		||||
	+firewall4 \
 | 
			
		||||
	+kmod-nft-tproxy
 | 
			
		||||
 | 
			
		||||
PKG_NAME:=luci-app-homeproxy
 | 
			
		||||
 | 
			
		||||
define Package/luci-app-homeproxy/conffiles
 | 
			
		||||
/etc/config/homeproxy
 | 
			
		||||
/etc/homeproxy/certs/
 | 
			
		||||
/etc/homeproxy/ruleset/
 | 
			
		||||
/etc/homeproxy/resources/direct_list.txt
 | 
			
		||||
/etc/homeproxy/resources/proxy_list.txt
 | 
			
		||||
/etc/homeproxy/cache.db
 | 
			
		||||
endef
 | 
			
		||||
 | 
			
		||||
include $(TOPDIR)/feeds/luci/luci.mk
 | 
			
		||||
 | 
			
		||||
# call BuildPackage - OpenWrt buildroot signature
 | 
			
		||||
@ -1,273 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
'require baseclass';
 | 
			
		||||
'require form';
 | 
			
		||||
'require fs';
 | 
			
		||||
'require rpc';
 | 
			
		||||
'require uci';
 | 
			
		||||
'require ui';
 | 
			
		||||
 | 
			
		||||
return baseclass.extend({
 | 
			
		||||
	dns_strategy: {
 | 
			
		||||
		'': _('Default'),
 | 
			
		||||
		'prefer_ipv4': _('Prefer IPv4'),
 | 
			
		||||
		'prefer_ipv6': _('Prefer IPv6'),
 | 
			
		||||
		'ipv4_only': _('IPv4 only'),
 | 
			
		||||
		'ipv6_only': _('IPv6 only')
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	shadowsocks_encrypt_methods: [
 | 
			
		||||
		/* Stream */
 | 
			
		||||
		'none',
 | 
			
		||||
		/* AEAD */
 | 
			
		||||
		'aes-128-gcm',
 | 
			
		||||
		'aes-192-gcm',
 | 
			
		||||
		'aes-256-gcm',
 | 
			
		||||
		'chacha20-ietf-poly1305',
 | 
			
		||||
		'xchacha20-ietf-poly1305',
 | 
			
		||||
		/* AEAD 2022 */
 | 
			
		||||
		'2022-blake3-aes-128-gcm',
 | 
			
		||||
		'2022-blake3-aes-256-gcm',
 | 
			
		||||
		'2022-blake3-chacha20-poly1305'
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	tls_cipher_suites: [
 | 
			
		||||
		'TLS_RSA_WITH_AES_128_CBC_SHA',
 | 
			
		||||
		'TLS_RSA_WITH_AES_256_CBC_SHA',
 | 
			
		||||
		'TLS_RSA_WITH_AES_128_GCM_SHA256',
 | 
			
		||||
		'TLS_RSA_WITH_AES_256_GCM_SHA384',
 | 
			
		||||
		'TLS_AES_128_GCM_SHA256',
 | 
			
		||||
		'TLS_AES_256_GCM_SHA384',
 | 
			
		||||
		'TLS_CHACHA20_POLY1305_SHA256',
 | 
			
		||||
		'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
 | 
			
		||||
		'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
 | 
			
		||||
		'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
 | 
			
		||||
		'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
 | 
			
		||||
		'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
 | 
			
		||||
		'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
 | 
			
		||||
		'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
 | 
			
		||||
		'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
 | 
			
		||||
		'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
 | 
			
		||||
		'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256'
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	tls_versions: [
 | 
			
		||||
		'1.0',
 | 
			
		||||
		'1.1',
 | 
			
		||||
		'1.2',
 | 
			
		||||
		'1.3'
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	calcStringMD5: function(e) {
 | 
			
		||||
		/* Thanks to https://stackoverflow.com/a/41602636 */
 | 
			
		||||
		function h(a, b) {
 | 
			
		||||
			var c, d, e, f, g;
 | 
			
		||||
			e = a & 2147483648;
 | 
			
		||||
			f = b & 2147483648;
 | 
			
		||||
			c = a & 1073741824;
 | 
			
		||||
			d = b & 1073741824;
 | 
			
		||||
			g = (a & 1073741823) + (b & 1073741823);
 | 
			
		||||
			return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f;
 | 
			
		||||
		}
 | 
			
		||||
		function k(a, b, c, d, e, f, g) { a = h(a, h(h(b & c | ~b & d, e), g)); return h(a << f | a >>> 32 - f, b); }
 | 
			
		||||
		function l(a, b, c, d, e, f, g) { a = h(a, h(h(b & d | c & ~d, e), g)); return h(a << f | a >>> 32 - f, b); }
 | 
			
		||||
		function m(a, b, d, c, e, f, g) { a = h(a, h(h(b ^ d ^ c, e), g)); return h(a << f | a >>> 32 - f, b); }
 | 
			
		||||
		function n(a, b, d, c, e, f, g) { a = h(a, h(h(d ^ (b | ~c), e), g)); return h(a << f | a >>> 32 - f, b); }
 | 
			
		||||
		function p(a) {
 | 
			
		||||
			var b = '', d = '';
 | 
			
		||||
			for (var c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2);
 | 
			
		||||
			return b;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var f = [], q, r, s, t, a, b, c, d;
 | 
			
		||||
		e = function(a) {
 | 
			
		||||
			a = a.replace(/\r\n/g, '\n');
 | 
			
		||||
			for (var b = '', d = 0; d < a.length; d++) {
 | 
			
		||||
				var c = a.charCodeAt(d);
 | 
			
		||||
				128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) :
 | 
			
		||||
					(b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)),
 | 
			
		||||
						b += String.fromCharCode(c & 63 | 128))
 | 
			
		||||
			}
 | 
			
		||||
			return b;
 | 
			
		||||
		}(e);
 | 
			
		||||
		f = function(b) {
 | 
			
		||||
			var c = b.length, a = c + 8;
 | 
			
		||||
			for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;)
 | 
			
		||||
				a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++;
 | 
			
		||||
			a = (g - g % 4) / 4; e[a] |= 128 << g % 4 * 8; e[d - 2] = c << 3; e[d - 1] = c >>> 29;
 | 
			
		||||
			return e;
 | 
			
		||||
		}(e);
 | 
			
		||||
		a = 1732584193;
 | 
			
		||||
		b = 4023233417;
 | 
			
		||||
		c = 2562383102;
 | 
			
		||||
		d = 271733878;
 | 
			
		||||
 | 
			
		||||
		for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d,
 | 
			
		||||
			a = k(a, b, c, d, f[e +  0],  7, 3614090360), d = k(d, a, b, c, f[e +  1], 12, 3905402710),
 | 
			
		||||
			c = k(c, d, a, b, f[e +  2], 17,  606105819), b = k(b, c, d, a, f[e +  3], 22, 3250441966),
 | 
			
		||||
			a = k(a, b, c, d, f[e +  4], 7,  4118548399), d = k(d, a, b, c, f[e +  5], 12, 1200080426),
 | 
			
		||||
			c = k(c, d, a, b, f[e +  6], 17, 2821735955), b = k(b, c, d, a, f[e +  7], 22, 4249261313),
 | 
			
		||||
			a = k(a, b, c, d, f[e +  8],  7, 1770035416), d = k(d, a, b, c, f[e +  9], 12, 2336552879),
 | 
			
		||||
			c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134),
 | 
			
		||||
			a = k(a, b, c, d, f[e + 12],  7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195),
 | 
			
		||||
			c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329),
 | 
			
		||||
			a = l(a, b, c, d, f[e +  1],  5, 4129170786), d = l(d, a, b, c, f[e +  6],  9, 3225465664),
 | 
			
		||||
			c = l(c, d, a, b, f[e + 11], 14,  643717713), b = l(b, c, d, a, f[e +  0], 20, 3921069994),
 | 
			
		||||
			a = l(a, b, c, d, f[e +  5],  5, 3593408605), d = l(d, a, b, c, f[e + 10],  9,   38016083),
 | 
			
		||||
			c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e +  4], 20, 3889429448),
 | 
			
		||||
			a = l(a, b, c, d, f[e +  9],  5,  568446438), d = l(d, a, b, c, f[e + 14],  9, 3275163606),
 | 
			
		||||
			c = l(c, d, a, b, f[e +  3], 14, 4107603335), b = l(b, c, d, a, f[e +  8], 20, 1163531501),
 | 
			
		||||
			a = l(a, b, c, d, f[e + 13],  5, 2850285829), d = l(d, a, b, c, f[e +  2],  9, 4243563512),
 | 
			
		||||
			c = l(c, d, a, b, f[e +  7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562),
 | 
			
		||||
			a = m(a, b, c, d, f[e +  5],  4, 4294588738), d = m(d, a, b, c, f[e +  8], 11, 2272392833),
 | 
			
		||||
			c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740),
 | 
			
		||||
			a = m(a, b, c, d, f[e +  1],  4, 2763975236), d = m(d, a, b, c, f[e +  4], 11, 1272893353),
 | 
			
		||||
			c = m(c, d, a, b, f[e +  7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656),
 | 
			
		||||
			a = m(a, b, c, d, f[e + 13],  4,  681279174), d = m(d, a, b, c, f[e +  0], 11, 3936430074),
 | 
			
		||||
			c = m(c, d, a, b, f[e +  3], 16, 3572445317), b = m(b, c, d, a, f[e +  6], 23,   76029189),
 | 
			
		||||
			a = m(a, b, c, d, f[e +  9],  4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461),
 | 
			
		||||
			c = m(c, d, a, b, f[e + 15], 16,  530742520), b = m(b, c, d, a, f[e +  2], 23, 3299628645),
 | 
			
		||||
			a = n(a, b, c, d, f[e +  0],  6, 4096336452), d = n(d, a, b, c, f[e +  7], 10, 1126891415),
 | 
			
		||||
			c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e +  5], 21, 4237533241),
 | 
			
		||||
			a = n(a, b, c, d, f[e + 12],  6, 1700485571), d = n(d, a, b, c, f[e +  3], 10, 2399980690),
 | 
			
		||||
			c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e +  1], 21, 2240044497),
 | 
			
		||||
			a = n(a, b, c, d, f[e +  8],  6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552),
 | 
			
		||||
			c = n(c, d, a, b, f[e +  6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649),
 | 
			
		||||
			a = n(a, b, c, d, f[e +  4],  6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917),
 | 
			
		||||
			c = n(c, d, a, b, f[e +  2], 15,  718787259), b = n(b, c, d, a, f[e +  9], 21, 3951481745),
 | 
			
		||||
			a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t);
 | 
			
		||||
		return (p(a) + p(b) + p(c) + p(d)).toLowerCase();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	decodeBase64Str: function(str) {
 | 
			
		||||
		if (!str)
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		/* Thanks to luci-app-ssr-plus */
 | 
			
		||||
		str = str.replace(/-/g, '+').replace(/_/g, '/');
 | 
			
		||||
		var padding = (4 - str.length % 4) % 4;
 | 
			
		||||
		if (padding)
 | 
			
		||||
			str = str + Array(padding + 1).join('=');
 | 
			
		||||
 | 
			
		||||
		return decodeURIComponent(Array.prototype.map.call(atob(str), (c) =>
 | 
			
		||||
			'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
 | 
			
		||||
		).join(''));
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	getBuiltinFeatures: function() {
 | 
			
		||||
		var callGetSingBoxFeatures = rpc.declare({
 | 
			
		||||
			object: 'luci.homeproxy',
 | 
			
		||||
			method: 'singbox_get_features',
 | 
			
		||||
			expect: { '': {} }
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return L.resolveDefault(callGetSingBoxFeatures(), {});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	generateUUIDv4: function() {
 | 
			
		||||
		/* Thanks to https://stackoverflow.com/a/2117523 */
 | 
			
		||||
		return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
 | 
			
		||||
			(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
 | 
			
		||||
		);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	loadDefaultLabel: function(uciconfig, ucisection) {
 | 
			
		||||
		var label = uci.get(uciconfig, ucisection, 'label');
 | 
			
		||||
		if (label) {
 | 
			
		||||
			return label;
 | 
			
		||||
		} else {
 | 
			
		||||
			uci.set(uciconfig, ucisection, 'label', ucisection);
 | 
			
		||||
			return ucisection;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	loadModalTitle: function(title, addtitle, uciconfig, ucisection) {
 | 
			
		||||
		var label = uci.get(uciconfig, ucisection, 'label');
 | 
			
		||||
		return label ? title + ' » ' + label : addtitle;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	renderSectionAdd: function(section, extra_class) {
 | 
			
		||||
		var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
 | 
			
		||||
			nameEl = el.querySelector('.cbi-section-create-name');
 | 
			
		||||
		ui.addValidator(nameEl, 'uciname', true, (v) => {
 | 
			
		||||
			var button = el.querySelector('.cbi-section-create > .cbi-button-add');
 | 
			
		||||
			var uciconfig = section.uciconfig || section.map.config;
 | 
			
		||||
 | 
			
		||||
			if (!v) {
 | 
			
		||||
				button.disabled = true;
 | 
			
		||||
				return true;
 | 
			
		||||
			} else if (uci.get(uciconfig, v)) {
 | 
			
		||||
				button.disabled = true;
 | 
			
		||||
				return _('Expecting: %s').format(_('unique UCI identifier'));
 | 
			
		||||
			} else {
 | 
			
		||||
				button.disabled = null;
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		}, 'blur', 'keyup');
 | 
			
		||||
 | 
			
		||||
		return el;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	uploadCertificate: function(option, type, filename, ev) {
 | 
			
		||||
		var callWriteCertificate = rpc.declare({
 | 
			
		||||
			object: 'luci.homeproxy',
 | 
			
		||||
			method: 'certificate_write',
 | 
			
		||||
			params: ['filename'],
 | 
			
		||||
			expect: { '': {} }
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target)
 | 
			
		||||
		.then(L.bind((btn, res) => {
 | 
			
		||||
			return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
 | 
			
		||||
				if (ret.result === true)
 | 
			
		||||
					ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
 | 
			
		||||
				else
 | 
			
		||||
					ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error)));
 | 
			
		||||
			});
 | 
			
		||||
		}, this, ev.target))
 | 
			
		||||
		.catch((e) => { ui.addNotification(null, E('p', e.message)) });
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	validateBase64Key: function(length, section_id, value) {
 | 
			
		||||
		/* Thanks to luci-proto-wireguard */
 | 
			
		||||
		if (section_id && value)
 | 
			
		||||
			if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
 | 
			
		||||
				return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
 | 
			
		||||
 | 
			
		||||
		return true;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	validateUniqueValue: function(uciconfig, ucisection, ucioption, section_id, value) {
 | 
			
		||||
		if (section_id) {
 | 
			
		||||
			if (!value)
 | 
			
		||||
				return _('Expecting: %s').format(_('non-empty value'));
 | 
			
		||||
 | 
			
		||||
			var duplicate = false;
 | 
			
		||||
			uci.sections(uciconfig, ucisection, (res) => {
 | 
			
		||||
				if (res['.name'] !== section_id)
 | 
			
		||||
					if (res[ucioption] === value)
 | 
			
		||||
						duplicate = true
 | 
			
		||||
			});
 | 
			
		||||
			if (duplicate)
 | 
			
		||||
				return _('Expecting: %s').format(_('unique value'));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return true;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	validateUUID: function(section_id, value) {
 | 
			
		||||
		if (section_id) {
 | 
			
		||||
			if (!value)
 | 
			
		||||
				return _('Expecting: %s').format(_('non-empty value'));
 | 
			
		||||
			else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
 | 
			
		||||
				return _('Expecting: %s').format(_('valid uuid'));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,740 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
'require form';
 | 
			
		||||
'require poll';
 | 
			
		||||
'require rpc';
 | 
			
		||||
'require uci';
 | 
			
		||||
'require view';
 | 
			
		||||
 | 
			
		||||
'require homeproxy as hp';
 | 
			
		||||
 | 
			
		||||
var callServiceList = rpc.declare({
 | 
			
		||||
	object: 'service',
 | 
			
		||||
	method: 'list',
 | 
			
		||||
	params: ['name'],
 | 
			
		||||
	expect: { '': {} }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function getServiceStatus() {
 | 
			
		||||
	return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
 | 
			
		||||
		var isRunning = false;
 | 
			
		||||
		try {
 | 
			
		||||
			isRunning = res['homeproxy']['instances']['sing-box-s']['running'];
 | 
			
		||||
		} catch (e) { }
 | 
			
		||||
		return isRunning;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(isRunning) {
 | 
			
		||||
	var spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
 | 
			
		||||
	var renderHTML;
 | 
			
		||||
	if (isRunning)
 | 
			
		||||
		renderHTML = spanTemp.format('green', _('HomeProxy Server'), _('RUNNING'));
 | 
			
		||||
	else
 | 
			
		||||
		renderHTML = spanTemp.format('red', _('HomeProxy Server'), _('NOT RUNNING'));
 | 
			
		||||
 | 
			
		||||
	return renderHTML;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
return view.extend({
 | 
			
		||||
	load: function() {
 | 
			
		||||
		return Promise.all([
 | 
			
		||||
			uci.load('homeproxy'),
 | 
			
		||||
			hp.getBuiltinFeatures()
 | 
			
		||||
		]);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	render: function(data) {
 | 
			
		||||
		var m, s, o;
 | 
			
		||||
		var features = data[1];
 | 
			
		||||
 | 
			
		||||
		m = new form.Map('homeproxy', _('HomeProxy Server'),
 | 
			
		||||
			_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
 | 
			
		||||
 | 
			
		||||
		s = m.section(form.TypedSection);
 | 
			
		||||
		s.render = function () {
 | 
			
		||||
			poll.add(function () {
 | 
			
		||||
				return L.resolveDefault(getServiceStatus()).then((res) => {
 | 
			
		||||
					var view = document.getElementById('service_status');
 | 
			
		||||
					view.innerHTML = renderStatus(res);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return E('div', { class: 'cbi-section', id: 'status_bar' }, [
 | 
			
		||||
					E('p', { id: 'service_status' }, _('Collecting data...'))
 | 
			
		||||
			]);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings'));
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'enabled', _('Enable'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'auto_firewall', _('Auto configure firewall'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
 | 
			
		||||
		s = m.section(form.GridSection, 'server', _('Server settings'));
 | 
			
		||||
		s.addremove = true;
 | 
			
		||||
		s.rowcolors = true;
 | 
			
		||||
		s.sortable = true;
 | 
			
		||||
		s.nodescriptions = true;
 | 
			
		||||
		s.modaltitle = L.bind(hp.loadModalTitle, this, _('Server'), _('Add a server'), data[0]);
 | 
			
		||||
		s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
 | 
			
		||||
		s.renderSectionAdd = L.bind(hp.renderSectionAdd, this, s);
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'label', _('Label'));
 | 
			
		||||
		o.load = L.bind(hp.loadDefaultLabel, this, data[0]);
 | 
			
		||||
		o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'label');
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'enabled', _('Enable'));
 | 
			
		||||
		o.default = o.enabled;
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.editable = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'type', _('Type'));
 | 
			
		||||
		o.value('http', _('HTTP'));
 | 
			
		||||
		if (features.with_quic) {
 | 
			
		||||
			o.value('hysteria', _('Hysteria'));
 | 
			
		||||
			o.value('hysteria2', _('Hysteria2'));
 | 
			
		||||
			o.value('naive', _('NaïveProxy'));
 | 
			
		||||
		}
 | 
			
		||||
		o.value('shadowsocks', _('Shadowsocks'));
 | 
			
		||||
		o.value('socks', _('Socks'));
 | 
			
		||||
		o.value('trojan', _('Trojan'));
 | 
			
		||||
		if (features.with_quic)
 | 
			
		||||
			o.value('tuic', _('Tuic'));
 | 
			
		||||
		o.value('vless', _('VLESS'));
 | 
			
		||||
		o.value('vmess', _('VMess'));
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'address', _('Listen address'));
 | 
			
		||||
		o.placeholder = '::';
 | 
			
		||||
		o.datatype = 'ipaddr';
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'port', _('Listen port'),
 | 
			
		||||
			_('The port must be unique.'));
 | 
			
		||||
		o.datatype = 'port';
 | 
			
		||||
		o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'port');
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'username', _('Username'));
 | 
			
		||||
		o.depends('type', 'http');
 | 
			
		||||
		o.depends('type', 'naive');
 | 
			
		||||
		o.depends('type', 'socks');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'password', _('Password'));
 | 
			
		||||
		o.password = true;
 | 
			
		||||
		o.depends({'type': /^(http|naive|socks)$/, 'username': /[\s\S]/});
 | 
			
		||||
		o.depends('type', 'hysteria2');
 | 
			
		||||
		o.depends('type', 'shadowsocks');
 | 
			
		||||
		o.depends('type', 'trojan');
 | 
			
		||||
		o.depends('type', 'tuic');
 | 
			
		||||
		o.validate = function(section_id, value) {
 | 
			
		||||
			if (section_id) {
 | 
			
		||||
				var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
 | 
			
		||||
				var required_type = [ 'http', 'naive', 'socks', 'shadowsocks' ];
 | 
			
		||||
 | 
			
		||||
				if (required_type.includes(type)) {
 | 
			
		||||
					if (type === 'shadowsocks') {
 | 
			
		||||
						var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id);
 | 
			
		||||
						if (encmode === 'none')
 | 
			
		||||
							return true;
 | 
			
		||||
						else if (encmode === '2022-blake3-aes-128-gcm')
 | 
			
		||||
							return hp.validateBase64Key(24, section_id, value);
 | 
			
		||||
						else if (['2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305'].includes(encmode))
 | 
			
		||||
							return hp.validateBase64Key(44, section_id, value);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (!value)
 | 
			
		||||
						return _('Expecting: %s').format(_('non-empty value'));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		/* Hysteria (2) config start */
 | 
			
		||||
		o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
 | 
			
		||||
		o.value('udp');
 | 
			
		||||
		/* WeChat-Video / FakeTCP are unsupported by sing-box currently
 | 
			
		||||
		   o.value('wechat-video');
 | 
			
		||||
		   o.value('faketcp');
 | 
			
		||||
		*/
 | 
			
		||||
		o.default = 'udp';
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'),
 | 
			
		||||
			_('Max download speed in Mbps.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.depends('type', 'hysteria2');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'),
 | 
			
		||||
			_('Max upload speed in Mbps.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.depends('type', 'hysteria2');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type'));
 | 
			
		||||
		o.value('', _('Disable'));
 | 
			
		||||
		o.value('base64', _('Base64'));
 | 
			
		||||
		o.value('string', _('String'));
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload'));
 | 
			
		||||
		o.depends({'type': 'hysteria', 'hysteria_auth_type': /[\s\S]/});
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
 | 
			
		||||
		o.value('', _('Disable'));
 | 
			
		||||
		o.value('salamander', _('Salamander'));
 | 
			
		||||
		o.depends('type', 'hysteria2');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_obfs_password', _('Obfuscate password'));
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'),
 | 
			
		||||
			_('The QUIC stream-level flow control window for receiving data.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.default = '67108864';
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_recv_window_client', _('QUIC connection receive window'),
 | 
			
		||||
			_('The QUIC connection-level flow control window for receiving data.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.default = '15728640';
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_max_conn_client', _('QUIC maximum concurrent bidirectional streams'),
 | 
			
		||||
			_('The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.default = '1024';
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'hysteria_disable_mtu_discovery', _('Disable Path MTU discovery'),
 | 
			
		||||
			_('Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
 | 
			
		||||
			_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''});
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'),
 | 
			
		||||
			_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
 | 
			
		||||
		o.depends('type', 'hysteria2');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
		/* Hysteria (2) config end */
 | 
			
		||||
 | 
			
		||||
		/* Shadowsocks config */
 | 
			
		||||
		o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
 | 
			
		||||
		for (var i of hp.shadowsocks_encrypt_methods)
 | 
			
		||||
			o.value(i);
 | 
			
		||||
		o.default = 'aes-128-gcm';
 | 
			
		||||
		o.depends('type', 'shadowsocks');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		/* Tuic config start */
 | 
			
		||||
		o = s.option(form.Value, 'uuid', _('UUID'));
 | 
			
		||||
		o.depends('type', 'tuic');
 | 
			
		||||
		o.depends('type', 'vless');
 | 
			
		||||
		o.depends('type', 'vmess');
 | 
			
		||||
		o.validate = hp.validateUUID;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'tuic_congestion_control', _('Congestion control algorithm'),
 | 
			
		||||
			_('QUIC congestion control algorithm.'));
 | 
			
		||||
		o.value('cubic');
 | 
			
		||||
		o.value('new_reno');
 | 
			
		||||
		o.value('bbr');
 | 
			
		||||
		o.default = 'cubic';
 | 
			
		||||
		o.depends('type', 'tuic');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'tuic_auth_timeout', _('Auth timeout'),
 | 
			
		||||
			_('How long the server should wait for the client to send the authentication command (in seconds).'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.default = '3';
 | 
			
		||||
		o.depends('type', 'tuic');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'tuic_enable_zero_rtt', _('Enable 0-RTT handshake'),
 | 
			
		||||
			_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
 | 
			
		||||
				'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends('type', 'tuic');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'tuic_heartbeat', _('Heartbeat interval'),
 | 
			
		||||
			_('Interval for sending heartbeat packets for keeping the connection alive (in seconds).'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.default = '10';
 | 
			
		||||
		o.depends('type', 'tuic');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
		/* Tuic config end */
 | 
			
		||||
 | 
			
		||||
		/* VLESS / VMess config start */
 | 
			
		||||
		o = s.option(form.ListValue, 'vless_flow', _('Flow'));
 | 
			
		||||
		o.value('', _('None'));
 | 
			
		||||
		o.value('xtls-rprx-vision');
 | 
			
		||||
		o.depends('type', 'vless');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'vmess_alterid', _('Alter ID'),
 | 
			
		||||
			_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.depends('type', 'vmess');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
		/* VMess config end */
 | 
			
		||||
 | 
			
		||||
		/* Transport config start */
 | 
			
		||||
		o = s.option(form.ListValue, 'transport', _('Transport'),
 | 
			
		||||
			_('No TCP transport, plain HTTP is merged into the HTTP transport.'));
 | 
			
		||||
		o.value('', _('None'));
 | 
			
		||||
		o.value('grpc', _('gRPC'));
 | 
			
		||||
		o.value('http', _('HTTP'));
 | 
			
		||||
		o.value('httpupgrade', _('HTTPUpgrade'));
 | 
			
		||||
		o.value('quic', _('QUIC'));
 | 
			
		||||
		o.value('ws', _('WebSocket'));
 | 
			
		||||
		o.depends('type', 'trojan');
 | 
			
		||||
		o.depends('type', 'vless');
 | 
			
		||||
		o.depends('type', 'vmess');
 | 
			
		||||
		o.onchange = function(ev, section_id, value) {
 | 
			
		||||
			var desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
 | 
			
		||||
			if (value === 'http')
 | 
			
		||||
				desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
 | 
			
		||||
			else if (value === 'quic')
 | 
			
		||||
				desc.innerHTML = _('No additional encryption support: It\'s basically duplicate encryption.');
 | 
			
		||||
			else
 | 
			
		||||
				desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
 | 
			
		||||
 | 
			
		||||
			var tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
 | 
			
		||||
			if ((value === 'http' && tls_element.checked) || (value === 'grpc' && !features.with_grpc))
 | 
			
		||||
				this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
 | 
			
		||||
					_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
 | 
			
		||||
			else if (value === 'grpc' && features.with_grpc)
 | 
			
		||||
				this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
 | 
			
		||||
					_('If the transport doesn\'t see any activity after a duration of this time (in seconds), it pings the client to check if the connection is still active.');
 | 
			
		||||
		}
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		/* gRPC config start */
 | 
			
		||||
		o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
 | 
			
		||||
		o.depends('transport', 'grpc');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		/* gRPC config end */
 | 
			
		||||
 | 
			
		||||
		/* HTTP(Upgrade) config start */
 | 
			
		||||
		o = s.option(form.DynamicList, 'http_host', _('Host'));
 | 
			
		||||
		o.datatype = 'hostname';
 | 
			
		||||
		o.depends('transport', 'http');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'httpupgrade_host', _('Host'));
 | 
			
		||||
		o.datatype = 'hostname';
 | 
			
		||||
		o.depends('transport', 'httpupgrade');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'http_path', _('Path'));
 | 
			
		||||
		o.depends('transport', 'http');
 | 
			
		||||
		o.depends('transport', 'httpupgrade');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'http_method', _('Method'));
 | 
			
		||||
		o.depends('transport', 'http');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
 | 
			
		||||
			_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.depends('transport', 'grpc');
 | 
			
		||||
		o.depends({'transport': 'http', 'tls': '1'});
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		if (features.with_grpc) {
 | 
			
		||||
			o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
 | 
			
		||||
				_('The timeout (in seconds) that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.'));
 | 
			
		||||
			o.datatype = 'uinteger';
 | 
			
		||||
			o.depends('transport', 'grpc');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
		}
 | 
			
		||||
		/* HTTP config end */
 | 
			
		||||
 | 
			
		||||
		/* WebSocket config start */
 | 
			
		||||
		o = s.option(form.Value, 'ws_host', _('Host'));
 | 
			
		||||
		o.depends('transport', 'ws');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'ws_path', _('Path'));
 | 
			
		||||
		o.depends('transport', 'ws');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'websocket_early_data', _('Early data'),
 | 
			
		||||
			_('Allowed payload size is in the request.'));
 | 
			
		||||
		o.datatype = 'uinteger';
 | 
			
		||||
		o.value('2048');
 | 
			
		||||
		o.depends('transport', 'ws');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'websocket_early_data_header', _('Early data header name'),
 | 
			
		||||
			_('Early data is sent in path instead of header by default.') +
 | 
			
		||||
			'<br/>' +
 | 
			
		||||
			_('To be compatible with Xray-core, set this to <code>Sec-WebSocket-Protocol</code>.'));
 | 
			
		||||
		o.value('Sec-WebSocket-Protocol');
 | 
			
		||||
		o.depends('transport', 'ws');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
		/* WebSocket config end */
 | 
			
		||||
 | 
			
		||||
		/* Transport config end */
 | 
			
		||||
 | 
			
		||||
		/* Mux config start */
 | 
			
		||||
		o = s.option(form.Flag, 'multiplex', _('Multiplex'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends('type', 'shadowsocks');
 | 
			
		||||
		o.depends('type', 'trojan');
 | 
			
		||||
		o.depends('type', 'vless');
 | 
			
		||||
		o.depends('type', 'vmess');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'multiplex_padding', _('Enable padding'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends('multiplex', '1');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		if (features.hp_has_tcp_brutal) {
 | 
			
		||||
			o = s.option(form.Flag, 'multiplex_brutal', _('Enable TCP Brutal'),
 | 
			
		||||
				_('Enable TCP Brutal congestion control algorithm'));
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends('multiplex', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'multiplex_brutal_down', _('Download bandwidth'),
 | 
			
		||||
				_('Download bandwidth in Mbps.'));
 | 
			
		||||
			o.datatype = 'uinteger';
 | 
			
		||||
			o.depends('multiplex_brutal', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'multiplex_brutal_up', _('Upload bandwidth'),
 | 
			
		||||
				_('Upload bandwidth in Mbps.'));
 | 
			
		||||
			o.datatype = 'uinteger';
 | 
			
		||||
			o.depends('multiplex_brutal', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
		}
 | 
			
		||||
		/* Mux config end */
 | 
			
		||||
 | 
			
		||||
		/* TLS config start */
 | 
			
		||||
		o = s.option(form.Flag, 'tls', _('TLS'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends('type', 'http');
 | 
			
		||||
		o.depends('type', 'hysteria');
 | 
			
		||||
		o.depends('type', 'hysteria2');
 | 
			
		||||
		o.depends('type', 'naive');
 | 
			
		||||
		o.depends('type', 'trojan');
 | 
			
		||||
		o.depends('type', 'vless');
 | 
			
		||||
		o.depends('type', 'vmess');
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.validate = function(section_id, value) {
 | 
			
		||||
			if (section_id) {
 | 
			
		||||
				var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
 | 
			
		||||
				var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
 | 
			
		||||
 | 
			
		||||
				if (['hysteria', 'hysteria2', 'tuic'].includes(type)) {
 | 
			
		||||
					tls.checked = true;
 | 
			
		||||
					tls.disabled = true;
 | 
			
		||||
				} else {
 | 
			
		||||
					tls.disabled = null;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'tls_sni', _('TLS SNI'),
 | 
			
		||||
			_('Used to verify the hostname on the returned certificates unless insecure is given.'));
 | 
			
		||||
		o.depends('tls', '1');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'),
 | 
			
		||||
			_('List of supported application level protocols, in order of preference.'));
 | 
			
		||||
		o.depends('tls', '1');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
 | 
			
		||||
			_('The minimum TLS version that is acceptable.'));
 | 
			
		||||
		o.value('', _('default'));
 | 
			
		||||
		for (var i of hp.tls_versions)
 | 
			
		||||
			o.value(i);
 | 
			
		||||
		o.depends('tls', '1');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
 | 
			
		||||
			_('The maximum TLS version that is acceptable.'));
 | 
			
		||||
		o.value('', _('default'));
 | 
			
		||||
		for (var i of hp.tls_versions)
 | 
			
		||||
			o.value(i);
 | 
			
		||||
		o.depends('tls', '1');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.MultiValue, 'tls_cipher_suites', _('Cipher suites'),
 | 
			
		||||
			_('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
 | 
			
		||||
		for (var i of hp.tls_cipher_suites)
 | 
			
		||||
			o.value(i);
 | 
			
		||||
		o.depends('tls', '1');
 | 
			
		||||
		o.optional = true;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		if (features.with_acme) {
 | 
			
		||||
			o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
 | 
			
		||||
				_('Use ACME TLS certificate issuer.'));
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends('tls', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains'));
 | 
			
		||||
			o.datatype = 'hostname';
 | 
			
		||||
			o.depends('tls_acme', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'),
 | 
			
		||||
				_('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.'));
 | 
			
		||||
			o.depends('tls_acme', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_email', _('Email'),
 | 
			
		||||
				_('The email address to use when creating or selecting an existing ACME server account.'));
 | 
			
		||||
			o.depends('tls_acme', '1');
 | 
			
		||||
			o.validate = function(section_id, value) {
 | 
			
		||||
				if (section_id) {
 | 
			
		||||
					if (!value)
 | 
			
		||||
						return _('Expecting: %s').format('non-empty value');
 | 
			
		||||
					else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
 | 
			
		||||
						return _('Expecting: %s').format('valid email address');
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_provider', _('CA provider'),
 | 
			
		||||
				_('The ACME CA provider to use.'));
 | 
			
		||||
			o.value('letsencrypt', _('Let\'s Encrypt'));
 | 
			
		||||
			o.value('zerossl', _('ZeroSSL'));
 | 
			
		||||
			o.depends('tls_acme', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Flag, 'tls_dns01_challenge', _('DNS01 challenge'))
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends('tls_acme', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
 | 
			
		||||
			o.value('alidns', _('Alibaba Cloud DNS'));
 | 
			
		||||
			o.value('cloudflare', _('Cloudflare'));
 | 
			
		||||
			o.depends('tls_dns01_challenge', '1');
 | 
			
		||||
			o.default = 'cloudflare';
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID'));
 | 
			
		||||
			o.depends('tls_dns01_provider', 'alidns');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret'));
 | 
			
		||||
			o.depends('tls_dns01_provider', 'alidns');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID'));
 | 
			
		||||
			o.depends('tls_dns01_provider', 'alidns');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
 | 
			
		||||
			o.depends('tls_dns01_provider', 'cloudflare');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends('tls_dns01_challenge', '0');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends('tls_dns01_challenge', '0');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'),
 | 
			
		||||
				_('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.'));
 | 
			
		||||
			o.datatype = 'port';
 | 
			
		||||
			o.depends('tls_dns01_challenge', '0');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'),
 | 
			
		||||
				_('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.'));
 | 
			
		||||
			o.datatype = 'port';
 | 
			
		||||
			o.depends('tls_dns01_challenge', '0');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'),
 | 
			
		||||
				_('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' +
 | 
			
		||||
				'<br/>External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.'));
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends('tls_acme', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
 | 
			
		||||
			o.depends('tls_acme_external_account', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key'));
 | 
			
		||||
			o.depends('tls_acme_external_account', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (features.with_reality_server) {
 | 
			
		||||
			o = s.option(form.Flag, 'tls_reality', _('REALITY'));
 | 
			
		||||
			o.default = o.disabled;
 | 
			
		||||
			o.depends({'tls': '1', 'tls_acme': '0', 'type': 'vless'});
 | 
			
		||||
			o.depends({'tls': '1', 'tls_acme': null, 'type': 'vless'});
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key'));
 | 
			
		||||
			o.depends('tls_reality', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
 | 
			
		||||
			o.depends('tls_reality', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
 | 
			
		||||
				_('The maximum time difference between the server and the client.'));
 | 
			
		||||
			o.depends('tls_reality', '1');
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
 | 
			
		||||
			o.datatype = 'hostname';
 | 
			
		||||
			o.depends('tls_reality', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
			o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
 | 
			
		||||
			o.datatype = 'port';
 | 
			
		||||
			o.depends('tls_reality', '1');
 | 
			
		||||
			o.rmempty = false;
 | 
			
		||||
			o.modalonly = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
 | 
			
		||||
			_('The server public key, in PEM format.'));
 | 
			
		||||
		o.value('/etc/homeproxy/certs/server_publickey.pem');
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
 | 
			
		||||
			_('<strong>Save your configuration before uploading files!</strong>'));
 | 
			
		||||
		o.inputstyle = 'action';
 | 
			
		||||
		o.inputtitle = _('Upload...');
 | 
			
		||||
		o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy/certs/server_publickey.pem'});
 | 
			
		||||
		o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'server_publickey');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Value, 'tls_key_path', _('Key path'),
 | 
			
		||||
			_('The server private key, in PEM format.'));
 | 
			
		||||
		o.value('/etc/homeproxy/certs/server_privatekey.pem');
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
 | 
			
		||||
		o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Button, '_upload_key', _('Upload key'),
 | 
			
		||||
			_('<strong>Save your configuration before uploading files!</strong>'));
 | 
			
		||||
		o.inputstyle = 'action';
 | 
			
		||||
		o.inputtitle = _('Upload...');
 | 
			
		||||
		o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy/certs/server_privatekey.pem'});
 | 
			
		||||
		o.onclick = L.bind(hp.uploadCertificate, this, _('private key'), 'server_privatekey');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
		/* TLS config end */
 | 
			
		||||
 | 
			
		||||
		/* Extra settings start */
 | 
			
		||||
		o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
 | 
			
		||||
			_('Enable tcp fast open for listener.'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends({'network': 'udp', '!reverse': true});
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends({'network': 'udp', '!reverse': true});
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
 | 
			
		||||
			_('Enable UDP fragmentation.'));
 | 
			
		||||
		o.default = o.disabled;
 | 
			
		||||
		o.depends({'network': 'tcp', '!reverse': true});
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.Flag, 'sniff_override', _('Override destination'),
 | 
			
		||||
			_('Override the connection destination address with the sniffed domain.'));
 | 
			
		||||
		o.rmempty = false;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
 | 
			
		||||
			_('If set, the requested domain name will be resolved to IP before routing.'));
 | 
			
		||||
		for (var i in hp.dns_strategy)
 | 
			
		||||
			o.value(i, hp.dns_strategy[i])
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.ListValue, 'network', _('Network'));
 | 
			
		||||
		o.value('tcp', _('TCP'));
 | 
			
		||||
		o.value('udp', _('UDP'));
 | 
			
		||||
		o.value('', _('Both'));
 | 
			
		||||
		o.depends('type', 'naive');
 | 
			
		||||
		o.depends('type', 'shadowsocks');
 | 
			
		||||
		o.modalonly = true;
 | 
			
		||||
		/* Extra settings end */
 | 
			
		||||
 | 
			
		||||
		return m.render();
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
@ -1,237 +0,0 @@
 | 
			
		||||
/* SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
'require dom';
 | 
			
		||||
'require form';
 | 
			
		||||
'require fs';
 | 
			
		||||
'require poll';
 | 
			
		||||
'require rpc';
 | 
			
		||||
'require uci';
 | 
			
		||||
'require ui';
 | 
			
		||||
'require view';
 | 
			
		||||
 | 
			
		||||
/* Thanks to luci-app-aria2 */
 | 
			
		||||
var css = '				\
 | 
			
		||||
#log_textarea {				\
 | 
			
		||||
	padding: 10px;			\
 | 
			
		||||
	text-align: left;		\
 | 
			
		||||
}					\
 | 
			
		||||
#log_textarea pre {			\
 | 
			
		||||
	padding: .5rem;			\
 | 
			
		||||
	word-break: break-all;		\
 | 
			
		||||
	margin: 0;			\
 | 
			
		||||
}					\
 | 
			
		||||
.description {				\
 | 
			
		||||
	background-color: #33ccff;	\
 | 
			
		||||
}';
 | 
			
		||||
 | 
			
		||||
var hp_dir = '/var/run/homeproxy';
 | 
			
		||||
 | 
			
		||||
function getConnStat(self, site) {
 | 
			
		||||
	var callConnStat = rpc.declare({
 | 
			
		||||
		object: 'luci.homeproxy',
 | 
			
		||||
		method: 'connection_check',
 | 
			
		||||
		params: ['site'],
 | 
			
		||||
		expect: { '': {} }
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	self.default = E('div', { 'style': 'cbi-value-field' }, [
 | 
			
		||||
		E('button', {
 | 
			
		||||
			'class': 'btn cbi-button cbi-button-action',
 | 
			
		||||
			'click': ui.createHandlerFn(this, function() {
 | 
			
		||||
				return L.resolveDefault(callConnStat(site), {}).then((ret) => {
 | 
			
		||||
                                        var ele = self.default.firstElementChild.nextElementSibling;
 | 
			
		||||
					if (ret.result) {
 | 
			
		||||
						ele.style.setProperty('color', 'green');
 | 
			
		||||
                                                ele.innerHTML = _('passed');
 | 
			
		||||
					} else {
 | 
			
		||||
						ele.style.setProperty('color', 'red');
 | 
			
		||||
                                                ele.innerHTML = _('failed');
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			})
 | 
			
		||||
		}, [ _('Check') ]),
 | 
			
		||||
		' ',
 | 
			
		||||
		E('strong', { 'style': 'color:gray' }, _('unchecked')),
 | 
			
		||||
	]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getResVersion(self, type) {
 | 
			
		||||
	var callResVersion = rpc.declare({
 | 
			
		||||
		object: 'luci.homeproxy',
 | 
			
		||||
		method: 'resources_get_version',
 | 
			
		||||
		params: ['type'],
 | 
			
		||||
		expect: { '': {} }
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	var callResUpdate = rpc.declare({
 | 
			
		||||
		object: 'luci.homeproxy',
 | 
			
		||||
		method: 'resources_update',
 | 
			
		||||
		params: ['type'],
 | 
			
		||||
		expect: { '': {} }
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return L.resolveDefault(callResVersion(type), {}).then((res) => {
 | 
			
		||||
		var spanTemp = E('div', { 'style': 'cbi-value-field' }, [
 | 
			
		||||
			E('button', {
 | 
			
		||||
				'class': 'btn cbi-button cbi-button-action',
 | 
			
		||||
				'click': ui.createHandlerFn(this, function() {
 | 
			
		||||
					return L.resolveDefault(callResUpdate(type), {}).then((res) => {
 | 
			
		||||
						switch (res.status) {
 | 
			
		||||
						case 0:
 | 
			
		||||
							self.description = _('Successfully updated.');
 | 
			
		||||
							break;
 | 
			
		||||
						case 1:
 | 
			
		||||
							self.description = _('Update failed.');
 | 
			
		||||
							break;
 | 
			
		||||
						case 2:
 | 
			
		||||
							self.description = _('Already in updating.');
 | 
			
		||||
							break;
 | 
			
		||||
						case 3:
 | 
			
		||||
							self.description = _('Already at the latest version.');
 | 
			
		||||
							break;
 | 
			
		||||
						default:
 | 
			
		||||
							self.description = _('Unknown error.');
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						return self.map.reset();
 | 
			
		||||
					});
 | 
			
		||||
				})
 | 
			
		||||
			}, [ _('Check update') ]),
 | 
			
		||||
			' ',
 | 
			
		||||
			E('strong', { 'style': (res.error ? 'color:red' : 'color:green') },
 | 
			
		||||
				[ res.error ? 'not found' : res.version ]
 | 
			
		||||
			),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		self.default = spanTemp;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRuntimeLog(name, filename) {
 | 
			
		||||
	var callLogClean = rpc.declare({
 | 
			
		||||
		object: 'luci.homeproxy',
 | 
			
		||||
		method: 'log_clean',
 | 
			
		||||
		params: ['type'],
 | 
			
		||||
		expect: { '': {} }
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	var log_textarea = E('div', { 'id': 'log_textarea' },
 | 
			
		||||
		E('img', {
 | 
			
		||||
			'src': L.resource(['icons/loading.gif']),
 | 
			
		||||
			'alt': _('Loading'),
 | 
			
		||||
			'style': 'vertical-align:middle'
 | 
			
		||||
		}, _('Collecting data...'))
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	var log;
 | 
			
		||||
	poll.add(L.bind(function() {
 | 
			
		||||
		return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text')
 | 
			
		||||
		.then(function(res) {
 | 
			
		||||
			log = E('pre', { 'wrap': 'pre' }, [
 | 
			
		||||
				res.trim() || _('Log is empty.')
 | 
			
		||||
			]);
 | 
			
		||||
 | 
			
		||||
			dom.content(log_textarea, log);
 | 
			
		||||
		}).catch(function(err) {
 | 
			
		||||
			if (err.toString().includes('NotFoundError'))
 | 
			
		||||
				log = E('pre', { 'wrap': 'pre' }, [
 | 
			
		||||
					_('Log file does not exist.')
 | 
			
		||||
				]);
 | 
			
		||||
			else
 | 
			
		||||
				log = E('pre', { 'wrap': 'pre' }, [
 | 
			
		||||
					_('Unknown error: %s').format(err)
 | 
			
		||||
				]);
 | 
			
		||||
 | 
			
		||||
			dom.content(log_textarea, log);
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	return E([
 | 
			
		||||
		E('style', [ css ]),
 | 
			
		||||
		E('div', {'class': 'cbi-map'}, [
 | 
			
		||||
			E('h3', {'name': 'content'}, [
 | 
			
		||||
				_('%s log').format(name),
 | 
			
		||||
				' ',
 | 
			
		||||
				E('button', {
 | 
			
		||||
					'class': 'btn cbi-button cbi-button-action',
 | 
			
		||||
					'click': ui.createHandlerFn(this, function() {
 | 
			
		||||
						return L.resolveDefault(callLogClean(filename), {});
 | 
			
		||||
					})
 | 
			
		||||
				}, [ _('Clean log') ])
 | 
			
		||||
			]),
 | 
			
		||||
			E('div', {'class': 'cbi-section'}, [
 | 
			
		||||
				log_textarea,
 | 
			
		||||
				E('div', {'style': 'text-align:right'},
 | 
			
		||||
					E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
 | 
			
		||||
				)
 | 
			
		||||
			])
 | 
			
		||||
		])
 | 
			
		||||
	]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
return view.extend({
 | 
			
		||||
	load: function() {
 | 
			
		||||
		return Promise.all([
 | 
			
		||||
			uci.load('homeproxy')
 | 
			
		||||
		]);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	render: function(data) {
 | 
			
		||||
		var m, s, o;
 | 
			
		||||
		var routing_mode = uci.get(data[0], 'config', 'routing_mode') || 'bypass_mainland_china';
 | 
			
		||||
 | 
			
		||||
		m = new form.Map('homeproxy');
 | 
			
		||||
 | 
			
		||||
		s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check'));
 | 
			
		||||
		s.anonymous = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
 | 
			
		||||
		o.cfgvalue = function() { return getConnStat(this, 'baidu') };
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_check_google', _('Google'));
 | 
			
		||||
		o.cfgvalue = function() { return getConnStat(this, 'google') };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
 | 
			
		||||
		s.anonymous = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
 | 
			
		||||
		o.cfgvalue = function() { return getResVersion(this, 'china_ip4') };
 | 
			
		||||
		o.rawhtml = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
 | 
			
		||||
		o.cfgvalue = function() { return getResVersion(this, 'china_ip6') };
 | 
			
		||||
		o.rawhtml = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_china_list_version', _('China list version'));
 | 
			
		||||
		o.cfgvalue = function() { return getResVersion(this, 'china_list') };
 | 
			
		||||
		o.rawhtml = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
 | 
			
		||||
		o.cfgvalue = function() { return getResVersion(this, 'gfw_list') };
 | 
			
		||||
		o.rawhtml = true;
 | 
			
		||||
 | 
			
		||||
		s = m.section(form.NamedSection, 'config', 'homeproxy');
 | 
			
		||||
		s.anonymous = true;
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_homeproxy_logview');
 | 
			
		||||
		o.render = L.bind(getRuntimeLog, this, _('HomeProxy'), 'homeproxy');
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_sing-box-c_logview');
 | 
			
		||||
		o.render = L.bind(getRuntimeLog, this, _('sing-box client'), 'sing-box-c');
 | 
			
		||||
 | 
			
		||||
		o = s.option(form.DummyValue, '_sing-box-s_logview');
 | 
			
		||||
		o.render = L.bind(getRuntimeLog, this, _('sing-box server'), 'sing-box-s');
 | 
			
		||||
 | 
			
		||||
		return m.render();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	handleSaveApply: null,
 | 
			
		||||
	handleSave: null,
 | 
			
		||||
	handleReset: null
 | 
			
		||||
});
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,32 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"bounding": [
 | 
			
		||||
		"CAP_NET_ADMIN",
 | 
			
		||||
		"CAP_NET_BIND_SERVICE",
 | 
			
		||||
		"CAP_NET_RAW",
 | 
			
		||||
		"CAP_SYS_PTRACE"
 | 
			
		||||
	],
 | 
			
		||||
	"effective": [
 | 
			
		||||
		"CAP_NET_ADMIN",
 | 
			
		||||
		"CAP_NET_BIND_SERVICE",
 | 
			
		||||
		"CAP_NET_RAW",
 | 
			
		||||
		"CAP_SYS_PTRACE"
 | 
			
		||||
	],
 | 
			
		||||
	"ambient": [
 | 
			
		||||
		"CAP_NET_ADMIN",
 | 
			
		||||
		"CAP_NET_BIND_SERVICE",
 | 
			
		||||
		"CAP_NET_RAW",
 | 
			
		||||
		"CAP_SYS_PTRACE"
 | 
			
		||||
	],
 | 
			
		||||
	"permitted": [
 | 
			
		||||
		"CAP_NET_ADMIN",
 | 
			
		||||
		"CAP_NET_BIND_SERVICE",
 | 
			
		||||
		"CAP_NET_RAW",
 | 
			
		||||
		"CAP_SYS_PTRACE"
 | 
			
		||||
	],
 | 
			
		||||
	"inheritable": [
 | 
			
		||||
		"CAP_NET_ADMIN",
 | 
			
		||||
		"CAP_NET_BIND_SERVICE",
 | 
			
		||||
		"CAP_NET_RAW",
 | 
			
		||||
		"CAP_SYS_PTRACE"
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
@ -1,72 +0,0 @@
 | 
			
		||||
 | 
			
		||||
config homeproxy 'infra'
 | 
			
		||||
	option __warning 'DO NOT EDIT THIS SECTION, OR YOU ARE ON YOUR OWN!'
 | 
			
		||||
	option common_port '22,53,80,143,443,465,853,873,993,995,8080,8443,9418'
 | 
			
		||||
	option mixed_port '5330'
 | 
			
		||||
	option redirect_port '5331'
 | 
			
		||||
	option tproxy_port '5332'
 | 
			
		||||
	option dns_port '5333'
 | 
			
		||||
	option china_dns_port '5334'
 | 
			
		||||
	option tun_name 'singtun0'
 | 
			
		||||
	option tun_addr4 '172.19.0.1/30'
 | 
			
		||||
	option tun_addr6 'fdfe:dcba:9876::1/126'
 | 
			
		||||
	option tun_mtu '9000'
 | 
			
		||||
	option table_mark '100'
 | 
			
		||||
	option self_mark '100'
 | 
			
		||||
	option tproxy_mark '101'
 | 
			
		||||
	option tun_mark '102'
 | 
			
		||||
 | 
			
		||||
config homeproxy 'config'
 | 
			
		||||
	option main_node 'nil'
 | 
			
		||||
	option main_udp_node 'same'
 | 
			
		||||
	option dns_server '8.8.8.8'
 | 
			
		||||
	option routing_mode 'bypass_mainland_china'
 | 
			
		||||
	option routing_port 'common'
 | 
			
		||||
	option proxy_mode 'redirect_tproxy'
 | 
			
		||||
	option ipv6_support '1'
 | 
			
		||||
 | 
			
		||||
config homeproxy 'control'
 | 
			
		||||
	option lan_proxy_mode 'disabled'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.105.192.0/23'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.108.4.0/22'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.108.8.0/22'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.108.16.0/22'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.108.12.0/22'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.108.20.0/22'
 | 
			
		||||
	list wan_proxy_ipv4_ips '91.108.56.0/22'
 | 
			
		||||
	list wan_proxy_ipv4_ips '149.154.160.0/20'
 | 
			
		||||
	list wan_proxy_ipv4_ips '185.76.151.0/24'
 | 
			
		||||
	list wan_proxy_ipv6_ips '2001:67c:4e8::/48'
 | 
			
		||||
	list wan_proxy_ipv6_ips '2001:b28:f23c::/48'
 | 
			
		||||
	list wan_proxy_ipv6_ips '2001:b28:f23d::/48'
 | 
			
		||||
	list wan_proxy_ipv6_ips '2001:b28:f23f::/48'
 | 
			
		||||
	list wan_proxy_ipv6_ips '2a0a:f280::/32'
 | 
			
		||||
 | 
			
		||||
config homeproxy 'routing'
 | 
			
		||||
	option sniff_override '1'
 | 
			
		||||
	option default_outbound 'direct-out'
 | 
			
		||||
 | 
			
		||||
config homeproxy 'dns'
 | 
			
		||||
	option dns_strategy 'prefer_ipv4'
 | 
			
		||||
	option default_server 'local-dns'
 | 
			
		||||
	option disable_cache '0'
 | 
			
		||||
	option disable_cache_expire '0'
 | 
			
		||||
 | 
			
		||||
config homeproxy 'subscription'
 | 
			
		||||
	option auto_update '0'
 | 
			
		||||
	option allow_insecure '0'
 | 
			
		||||
	option packet_encoding 'xudp'
 | 
			
		||||
	option update_via_proxy '0'
 | 
			
		||||
	option filter_nodes 'disabled'
 | 
			
		||||
 | 
			
		||||
config homeproxy 'server'
 | 
			
		||||
	option enabled '0'
 | 
			
		||||
	option auto_firewall '0'
 | 
			
		||||
 | 
			
		||||
config dns_rule 'nodes_domain'
 | 
			
		||||
	option label 'NodesDomain'
 | 
			
		||||
	option enabled '1'
 | 
			
		||||
	option mode 'default'
 | 
			
		||||
	list outbound 'any-out'
 | 
			
		||||
	option server 'default-dns'
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1 +0,0 @@
 | 
			
		||||
20240802150027
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1 +0,0 @@
 | 
			
		||||
20240802150027
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1 +0,0 @@
 | 
			
		||||
202408012210
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1 +0,0 @@
 | 
			
		||||
202408012210
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
# SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 | 
			
		||||
NAME="homeproxy"
 | 
			
		||||
 | 
			
		||||
log_max_size="10" #KB
 | 
			
		||||
main_log_file="/var/run/$NAME/$NAME.log"
 | 
			
		||||
singc_log_file="/var/run/$NAME/sing-box-c.log"
 | 
			
		||||
sings_log_file="/var/run/$NAME/sing-box-s.log"
 | 
			
		||||
 | 
			
		||||
while true; do
 | 
			
		||||
	sleep 180
 | 
			
		||||
	for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do
 | 
			
		||||
		[ -s "$i" ] || continue
 | 
			
		||||
		[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
 | 
			
		||||
	done
 | 
			
		||||
done
 | 
			
		||||
@ -1,632 +0,0 @@
 | 
			
		||||
#!/usr/bin/utpl
 | 
			
		||||
 | 
			
		||||
{%-
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
import { readfile } from 'fs';
 | 
			
		||||
import { cursor } from 'uci';
 | 
			
		||||
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
 | 
			
		||||
 | 
			
		||||
const fw4 = require('fw4');
 | 
			
		||||
 | 
			
		||||
function array_to_nftarr(array) {
 | 
			
		||||
	if (type(array) !== 'array')
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	return `{ ${join(', ', uniq(array))} }`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resolve_ipv6(str) {
 | 
			
		||||
	if (isEmpty(str))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	let ipv6 = fw4.parse_subnet(str)?.[0];
 | 
			
		||||
	if (!ipv6 || ipv6.family !== 6)
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	if (ipv6.bits > -1)
 | 
			
		||||
		return `${ipv6.addr}/${ipv6.bits}`;
 | 
			
		||||
	else
 | 
			
		||||
		return `& ${ipv6.mask} == ${ipv6.addr}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Misc config */
 | 
			
		||||
const resources_dir = '/etc/homeproxy/resources';
 | 
			
		||||
 | 
			
		||||
/* UCI config start */
 | 
			
		||||
const cfgname = 'homeproxy';
 | 
			
		||||
const uci = cursor();
 | 
			
		||||
uci.load(cfgname);
 | 
			
		||||
 | 
			
		||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
 | 
			
		||||
let outbound_node, outbound_udp_node, china_dns_server, bypass_cn_traffic;
 | 
			
		||||
 | 
			
		||||
if (routing_mode !== 'custom') {
 | 
			
		||||
	outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
 | 
			
		||||
	outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
 | 
			
		||||
	china_dns_server = uci.get(cfgname, 'config', 'china_dns_server');
 | 
			
		||||
} else {
 | 
			
		||||
	outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
 | 
			
		||||
	bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let routing_port = uci.get(cfgname, 'config', 'routing_port') || 'common';
 | 
			
		||||
if (routing_port === 'common')
 | 
			
		||||
	routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418';
 | 
			
		||||
 | 
			
		||||
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
 | 
			
		||||
      ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
 | 
			
		||||
 | 
			
		||||
let self_mark, redirect_port,
 | 
			
		||||
    tproxy_port, tproxy_mark,
 | 
			
		||||
    tun_name, tun_mark;
 | 
			
		||||
 | 
			
		||||
if (match(proxy_mode, /redirect/)) {
 | 
			
		||||
	self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100';
 | 
			
		||||
	redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331';
 | 
			
		||||
}
 | 
			
		||||
if (match(proxy_mode, /tproxy/))
 | 
			
		||||
	if (outbound_udp_node !== 'nil' || routing_mode === 'custom') {
 | 
			
		||||
		tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332';
 | 
			
		||||
		tproxy_mark = uci.get(cfgname, 'infra', 'tproxy_mark') || '101';
 | 
			
		||||
	}
 | 
			
		||||
if (match(proxy_mode, /tun/)) {
 | 
			
		||||
	tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
 | 
			
		||||
	tun_mark = uci.get(cfgname, 'infra', 'tun_mark') || '102';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const control_options = [
 | 
			
		||||
	"listen_interfaces", "lan_proxy_mode",
 | 
			
		||||
	"lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips",
 | 
			
		||||
	"lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips",
 | 
			
		||||
	"lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips",
 | 
			
		||||
	"lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips",
 | 
			
		||||
	"wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips",
 | 
			
		||||
	"wan_direct_ipv4_ips", "wan_direct_ipv6_ips"
 | 
			
		||||
];
 | 
			
		||||
const control_info = {};
 | 
			
		||||
 | 
			
		||||
for (let i in control_options)
 | 
			
		||||
	control_info[i] = uci.get(cfgname, 'control', i);
 | 
			
		||||
/* UCI config end */
 | 
			
		||||
-%}
 | 
			
		||||
 | 
			
		||||
{# Reserved addresses -#}
 | 
			
		||||
set homeproxy_local_addr_v4 {
 | 
			
		||||
	type ipv4_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
	elements = {
 | 
			
		||||
		0.0.0.0/8,
 | 
			
		||||
		10.0.0.0/8,
 | 
			
		||||
		100.64.0.0/10,
 | 
			
		||||
		127.0.0.0/8,
 | 
			
		||||
		169.254.0.0/16,
 | 
			
		||||
		172.16.0.0/12,
 | 
			
		||||
		192.0.0.0/24,
 | 
			
		||||
		192.0.2.0/24,
 | 
			
		||||
		192.31.196.0/24,
 | 
			
		||||
		192.52.193.0/24,
 | 
			
		||||
		192.88.99.0/24,
 | 
			
		||||
		192.168.0.0/16,
 | 
			
		||||
		192.175.48.0/24,
 | 
			
		||||
		198.18.0.0/15,
 | 
			
		||||
		198.51.100.0/24,
 | 
			
		||||
		203.0.113.0/24,
 | 
			
		||||
		224.0.0.0/4,
 | 
			
		||||
		240.0.0.0/4
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
{% if (ipv6_support === '1'): %}
 | 
			
		||||
set homeproxy_local_addr_v6 {
 | 
			
		||||
	type ipv6_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
	elements = {
 | 
			
		||||
		::/128,
 | 
			
		||||
		::1/128,
 | 
			
		||||
		::ffff:0:0/96,
 | 
			
		||||
		100::/64,
 | 
			
		||||
		64:ff9b::/96,
 | 
			
		||||
		2001::/32,
 | 
			
		||||
		2001:10::/28,
 | 
			
		||||
		2001:20::/28,
 | 
			
		||||
		2001:db8::/28,
 | 
			
		||||
		2002::/16,
 | 
			
		||||
		fc00::/7,
 | 
			
		||||
		fe80::/10,
 | 
			
		||||
		ff00::/8
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if (routing_mode === 'gfwlist'): %}
 | 
			
		||||
set homeproxy_gfw_list_v4 {
 | 
			
		||||
	type ipv4_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
}
 | 
			
		||||
{% if (ipv6_support === '1'): %}
 | 
			
		||||
set homeproxy_gfw_list_v6 {
 | 
			
		||||
	type ipv6_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
}
 | 
			
		||||
{% endif /* ipv6_support */ %}
 | 
			
		||||
{% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %}
 | 
			
		||||
set homeproxy_mainland_addr_v4 {
 | 
			
		||||
	type ipv4_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
	elements = {
 | 
			
		||||
		{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %}
 | 
			
		||||
		{{ cnip4 }},
 | 
			
		||||
		{% endfor %}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
{% if ((ipv6_support === '1') || china_dns_server): %}
 | 
			
		||||
set homeproxy_mainland_addr_v6 {
 | 
			
		||||
	type ipv6_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
	elements = {
 | 
			
		||||
		{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %}
 | 
			
		||||
		{{ cnip6 }},
 | 
			
		||||
		{% endfor %}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
{% endif /* ipv6_support */ %}
 | 
			
		||||
{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
{# WAN ACL addresses #}
 | 
			
		||||
set homeproxy_wan_proxy_addr_v4 {
 | 
			
		||||
	type ipv4_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
{% if (control_info.wan_proxy_ipv4_ips): %}
 | 
			
		||||
	elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} }
 | 
			
		||||
{% endif %}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
{% if (ipv6_support === '1'): %}
 | 
			
		||||
set homeproxy_wan_proxy_addr_v6 {
 | 
			
		||||
	type ipv6_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
{% if (control_info.wan_proxy_ipv6_ips): %}
 | 
			
		||||
	elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} }
 | 
			
		||||
{% endif /* wan_proxy_ipv6_ips*/ %}
 | 
			
		||||
}
 | 
			
		||||
{% endif /* ipv6_support */ %}
 | 
			
		||||
 | 
			
		||||
set homeproxy_wan_direct_addr_v4 {
 | 
			
		||||
	type ipv4_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
{% if (control_info.wan_direct_ipv4_ips): %}
 | 
			
		||||
	elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} }
 | 
			
		||||
{% endif %}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
{% if (ipv6_support === '1'): %}
 | 
			
		||||
set homeproxy_wan_direct_addr_v6 {
 | 
			
		||||
	type ipv6_addr
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
{% if (control_info.wan_direct_ipv6_ips): %}
 | 
			
		||||
	elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} }
 | 
			
		||||
{% endif /* wan_direct_ipv6_ips */ %}
 | 
			
		||||
}
 | 
			
		||||
{% endif /* ipv6_support */ %}
 | 
			
		||||
 | 
			
		||||
{% if (routing_port !== 'all'): %}
 | 
			
		||||
set homeproxy_routing_port {
 | 
			
		||||
	type inet_service
 | 
			
		||||
	flags interval
 | 
			
		||||
	auto-merge
 | 
			
		||||
	elements = { {{ join(', ', split(routing_port, ',')) }} }
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{# TCP redirect #}
 | 
			
		||||
{% if (match(proxy_mode, /redirect/)): %}
 | 
			
		||||
chain homeproxy_redirect_proxy {
 | 
			
		||||
	meta l4proto tcp counter redirect to :{{ redirect_port }}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_redirect_proxy_port {
 | 
			
		||||
	{% if (routing_port !== 'all'): %}
 | 
			
		||||
	tcp dport != @homeproxy_routing_port counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
	goto homeproxy_redirect_proxy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_redirect_lanac {
 | 
			
		||||
	{% if (control_info.listen_interfaces): %}
 | 
			
		||||
	meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
	meta mark {{ self_mark }} counter return
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.lan_proxy_mode === 'listed_only'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect
 | 
			
		||||
	{% endif /* lan_proxy_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect
 | 
			
		||||
	{% endfor /* lan_proxy_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect
 | 
			
		||||
	{% endif /* lan_proxy_mac_addrs */ %}
 | 
			
		||||
	{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
 | 
			
		||||
	{% endif /* lan_direct_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
 | 
			
		||||
	{% endfor /* lan_direct_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
 | 
			
		||||
	{% endif /* lan_direct_mac_addrs */ %}
 | 
			
		||||
	{% endif /* lan_proxy_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
 | 
			
		||||
	counter goto homeproxy_redirect
 | 
			
		||||
	{% endif %}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_redirect {
 | 
			
		||||
	meta mark {{ self_mark }} counter return
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_local_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_local_addr_v6 counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode !== 'custom'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port
 | 
			
		||||
	{% endif /* lan_global_proxy_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port
 | 
			
		||||
	{% endfor /* lan_global_proxy_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port
 | 
			
		||||
	{% endif /* lan_global_proxy_mac_addrs */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_wan_direct_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode === 'gfwlist'): %}
 | 
			
		||||
	ip daddr != @homeproxy_gfw_list_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_gfw_list_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
 | 
			
		||||
	ip daddr @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% elif (routing_mode === 'proxy_mainland_china'): %}
 | 
			
		||||
	ip daddr != @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy
 | 
			
		||||
	{% endif /* lan_gaming_mode_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy
 | 
			
		||||
	{% endfor /* lan_gaming_mode_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy
 | 
			
		||||
	{% endif /* lan_gaming_mode_mac_addrs */ %}
 | 
			
		||||
 | 
			
		||||
	counter goto homeproxy_redirect_proxy_port
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_output_redir {
 | 
			
		||||
	type nat hook output priority filter -105; policy accept
 | 
			
		||||
	meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain dstnat {
 | 
			
		||||
	meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{# UDP tproxy #}
 | 
			
		||||
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %}
 | 
			
		||||
chain homeproxy_mangle_tproxy {
 | 
			
		||||
	meta l4proto udp mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	meta l4proto udp mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
 | 
			
		||||
	{% endif %}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_tproxy_port {
 | 
			
		||||
	{% if (routing_port !== 'all'): %}
 | 
			
		||||
	udp dport != @homeproxy_routing_port counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
	goto homeproxy_mangle_tproxy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_mark {
 | 
			
		||||
	{% if (routing_port !== 'all'): %}
 | 
			
		||||
	udp dport != @homeproxy_routing_port counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
	meta l4proto udp mark set {{ tproxy_mark }} counter accept
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_lanac {
 | 
			
		||||
	{% if (control_info.listen_interfaces): %}
 | 
			
		||||
	meta iifname != {{ array_to_nftarr(split(join(' ', control_info.listen_interfaces) + ' lo', ' ')) }} counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
	meta mark {{ self_mark }} counter return
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.lan_proxy_mode === 'listed_only'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting
 | 
			
		||||
	{% endif /* lan_proxy_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting
 | 
			
		||||
	{% endfor /* lan_proxy_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting
 | 
			
		||||
	{% endif /* lan_proxy_mac_addrs */ %}
 | 
			
		||||
	{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
 | 
			
		||||
	{% endif /* lan_direct_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
 | 
			
		||||
	{% endfor /* lan_direct_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
 | 
			
		||||
	{% endif /* lan_direct_mac_addrs */ %}
 | 
			
		||||
	{% endif /* lan_proxy_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
 | 
			
		||||
	counter goto homeproxy_mangle_prerouting
 | 
			
		||||
	{% endif %}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_prerouting {
 | 
			
		||||
	ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_local_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_local_addr_v6 counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode !== 'custom'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port
 | 
			
		||||
	{% endif /* lan_global_proxy_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port
 | 
			
		||||
	{% endfor /* lan_global_proxy_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port
 | 
			
		||||
	{% endif /* lan_global_proxy_mac_addrs */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_wan_direct_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode === 'gfwlist'): %}
 | 
			
		||||
	ip daddr != @homeproxy_gfw_list_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_gfw_list_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
 | 
			
		||||
	{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
 | 
			
		||||
	ip daddr @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% if (routing_mode !== 'custom'): %}
 | 
			
		||||
	udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
	{% elif (routing_mode === 'proxy_mainland_china'): %}
 | 
			
		||||
	ip daddr != @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy
 | 
			
		||||
	{% endif /* lan_gaming_mode_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy
 | 
			
		||||
	{% endfor /* lan_gaming_mode_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy
 | 
			
		||||
	{% endif /* lan_gaming_mode_mac_addrs */ %}
 | 
			
		||||
 | 
			
		||||
	counter goto homeproxy_mangle_tproxy_port
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_output {
 | 
			
		||||
	meta mark {{ self_mark }} counter return
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_local_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_local_addr_v6 counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_wan_direct_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode === 'gfwlist'): %}
 | 
			
		||||
	ip daddr != @homeproxy_gfw_list_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_gfw_list_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
 | 
			
		||||
	ip daddr @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% elif (routing_mode === 'proxy_mainland_china'): %}
 | 
			
		||||
	ip daddr != @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	counter goto homeproxy_mangle_mark
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain mangle_prerouting {
 | 
			
		||||
	meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain mangle_output {
 | 
			
		||||
	meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{# TUN #}
 | 
			
		||||
{% if (match(proxy_mode, /tun/)): %}
 | 
			
		||||
chain homeproxy_mangle_lanac {
 | 
			
		||||
	iifname {{ tun_name }} counter return
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.listen_interfaces): %}
 | 
			
		||||
	meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.lan_proxy_mode === 'listed_only'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun
 | 
			
		||||
	{% endif /* lan_proxy_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun
 | 
			
		||||
	{% endfor /* lan_proxy_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun
 | 
			
		||||
	{% endif /* lan_proxy_mac_addrs */ %}
 | 
			
		||||
	{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
 | 
			
		||||
	{% endif /* lan_direct_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
 | 
			
		||||
	{% endfor /* lan_direct_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
 | 
			
		||||
	{% endif /* lan_direct_mac_addrs */ %}
 | 
			
		||||
	{% endif /* lan_proxy_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
 | 
			
		||||
	counter goto homeproxy_mangle_tun
 | 
			
		||||
	{% endif %}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_tun_mark {
 | 
			
		||||
	{% if (routing_port !== 'all'): %}
 | 
			
		||||
	{% if (proxy_mode === 'tun'): %}
 | 
			
		||||
	tcp dport != @homeproxy_routing_port counter return
 | 
			
		||||
	{% endif /* proxy_mode */ %}
 | 
			
		||||
	udp dport != @homeproxy_routing_port counter return
 | 
			
		||||
	{% endif /* routing_port */ %}
 | 
			
		||||
 | 
			
		||||
	counter mark set {{ tun_mark }}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain homeproxy_mangle_tun {
 | 
			
		||||
	iifname {{ tun_name }} counter return
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	ip daddr @homeproxy_local_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_local_addr_v6 counter return
 | 
			
		||||
	{% endif %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode !== 'custom'): %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark
 | 
			
		||||
	{% endif /* lan_global_proxy_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark
 | 
			
		||||
	{% endfor /* lan_global_proxy_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark
 | 
			
		||||
	{% endif /* lan_global_proxy_mac_addrs */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (control_info.wan_direct_ipv4_ips): %}
 | 
			
		||||
	ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return
 | 
			
		||||
	{% endif /* wan_direct_ipv4_ips */ %}
 | 
			
		||||
	{% if (control_info.wan_direct_ipv6_ips): %}
 | 
			
		||||
	ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return
 | 
			
		||||
	{% endif /* wan_direct_ipv6_ips */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (routing_mode === 'gfwlist'): %}
 | 
			
		||||
	ip daddr != @homeproxy_gfw_list_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_gfw_list_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
 | 
			
		||||
	{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
 | 
			
		||||
	ip daddr @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% if (routing_mode !== 'custom'): %}
 | 
			
		||||
	udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
	{% elif (routing_mode === 'proxy_mainland_china'): %}
 | 
			
		||||
	ip daddr != @homeproxy_mainland_addr_v4 counter return
 | 
			
		||||
	{% if (ipv6_support === '1'): %}
 | 
			
		||||
	ip6 daddr != @homeproxy_mainland_addr_v6 counter return
 | 
			
		||||
	{% endif /* ipv6_support */ %}
 | 
			
		||||
	{% endif /* routing_mode */ %}
 | 
			
		||||
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
 | 
			
		||||
	ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter mark set {{ tun_mark }}
 | 
			
		||||
	{% endif /* lan_gaming_mode_ipv4_ips */ %}
 | 
			
		||||
	{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
 | 
			
		||||
	ip6 saddr {{ resolve_ipv6(ipv6) }} counter mark set {{ tun_mark }}
 | 
			
		||||
	{% endfor /* lan_gaming_mode_ipv6_ips */ %}
 | 
			
		||||
	{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
 | 
			
		||||
	ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter mark set {{ tun_mark }}
 | 
			
		||||
	{% endif /* lan_gaming_mode_mac_addrs */ %}
 | 
			
		||||
 | 
			
		||||
	counter goto homeproxy_mangle_tun_mark
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain mangle_prerouting {
 | 
			
		||||
	meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
chain mangle_output {
 | 
			
		||||
	meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
#!/usr/bin/utpl -S
 | 
			
		||||
 | 
			
		||||
{%-
 | 
			
		||||
	import { cursor } from 'uci';
 | 
			
		||||
 | 
			
		||||
	const cfgname = 'homeproxy';
 | 
			
		||||
	const uci = cursor();
 | 
			
		||||
	uci.load(cfgname);
 | 
			
		||||
 | 
			
		||||
	const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china',
 | 
			
		||||
	      proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy';
 | 
			
		||||
 | 
			
		||||
	let outbound_node, tun_name;
 | 
			
		||||
	if (match(proxy_mode, /tun/)) {
 | 
			
		||||
		if (routing_mode === 'custom')
 | 
			
		||||
			outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
 | 
			
		||||
		else
 | 
			
		||||
			outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
 | 
			
		||||
 | 
			
		||||
		if (outbound_node !== 'nil')
 | 
			
		||||
			tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const server_enabled = uci.get(cfgname, 'server', 'enabled');
 | 
			
		||||
	let auto_firewall = '0';
 | 
			
		||||
	if (server_enabled === '1')
 | 
			
		||||
		auto_firewall = uci.get(cfgname, 'server', 'auto_firewall') || '0';
 | 
			
		||||
 | 
			
		||||
-%}
 | 
			
		||||
 | 
			
		||||
{% if (tun_name): %}
 | 
			
		||||
chain forward {
 | 
			
		||||
	oifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun forward"
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if (tun_name || auto_firewall === '1'): %}
 | 
			
		||||
chain input {
 | 
			
		||||
	{% if (tun_name): %}
 | 
			
		||||
	iifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun input"
 | 
			
		||||
	{% endif %}
 | 
			
		||||
{%
 | 
			
		||||
	if (auto_firewall === '1')
 | 
			
		||||
		uci.foreach(cfgname, 'server', (s) => {
 | 
			
		||||
			if (s.enabled !== '1')
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			let proto = s.network || '{ tcp, udp }';
 | 
			
		||||
			printf('	meta l4proto %s th dport %s counter accept comment "!%s: accept server %s"\n',
 | 
			
		||||
				proto, s.port, cfgname, s['.name']);
 | 
			
		||||
		});
 | 
			
		||||
%}
 | 
			
		||||
}
 | 
			
		||||
{% endif %}
 | 
			
		||||
@ -1,673 +0,0 @@
 | 
			
		||||
#!/usr/bin/ucode
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
import { readfile, writefile } from 'fs';
 | 
			
		||||
import { isnan } from 'math';
 | 
			
		||||
import { cursor } from 'uci';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
	executeCommand, isEmpty, strToBool, strToInt,
 | 
			
		||||
	removeBlankAttrs, validateHostname, validation,
 | 
			
		||||
	HP_DIR, RUN_DIR
 | 
			
		||||
} from 'homeproxy';
 | 
			
		||||
 | 
			
		||||
/* UCI config start */
 | 
			
		||||
const uci = cursor();
 | 
			
		||||
 | 
			
		||||
const uciconfig = 'homeproxy';
 | 
			
		||||
uci.load(uciconfig);
 | 
			
		||||
 | 
			
		||||
const uciinfra = 'infra',
 | 
			
		||||
      ucimain = 'config',
 | 
			
		||||
      uciexp = 'experimental',
 | 
			
		||||
      ucicontrol = 'control';
 | 
			
		||||
 | 
			
		||||
const ucidnssetting = 'dns',
 | 
			
		||||
      ucidnsserver = 'dns_server',
 | 
			
		||||
      ucidnsrule = 'dns_rule';
 | 
			
		||||
 | 
			
		||||
const uciroutingsetting = 'routing',
 | 
			
		||||
      uciroutingnode = 'routing_node',
 | 
			
		||||
      uciroutingrule = 'routing_rule';
 | 
			
		||||
 | 
			
		||||
const ucinode = 'node';
 | 
			
		||||
const uciruleset = 'ruleset';
 | 
			
		||||
 | 
			
		||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
 | 
			
		||||
 | 
			
		||||
let wan_dns = executeCommand('ifstatus wan | jsonfilter -e \'@["dns-server"][0]\'');
 | 
			
		||||
if (wan_dns.exitcode === 0 && trim(wan_dns.stdout))
 | 
			
		||||
	wan_dns = trim(wan_dns.stdout);
 | 
			
		||||
else
 | 
			
		||||
	wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '208.67.222.222' : '114.114.114.114';
 | 
			
		||||
 | 
			
		||||
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
 | 
			
		||||
 | 
			
		||||
let main_node, main_udp_node, dedicated_udp_node, default_outbound, sniff_override = '1',
 | 
			
		||||
    dns_server, dns_default_strategy, dns_default_server, dns_disable_cache, dns_disable_cache_expire,
 | 
			
		||||
    dns_independent_cache, dns_client_subnet, direct_domain_list, proxy_domain_list;
 | 
			
		||||
 | 
			
		||||
if (routing_mode !== 'custom') {
 | 
			
		||||
	main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
 | 
			
		||||
	main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
 | 
			
		||||
	dedicated_udp_node = !isEmpty(main_udp_node) && !(main_udp_node in ['same', main_node]);
 | 
			
		||||
 | 
			
		||||
	dns_server = uci.get(uciconfig, ucimain, 'dns_server');
 | 
			
		||||
	if (isEmpty(dns_server) || dns_server === 'wan')
 | 
			
		||||
		dns_server = wan_dns;
 | 
			
		||||
 | 
			
		||||
	direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt'));
 | 
			
		||||
	if (direct_domain_list)
 | 
			
		||||
		direct_domain_list = split(direct_domain_list, /[\r\n]/);
 | 
			
		||||
 | 
			
		||||
	proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt'));
 | 
			
		||||
	if (proxy_domain_list)
 | 
			
		||||
		proxy_domain_list = split(proxy_domain_list, /[\r\n]/);
 | 
			
		||||
} else {
 | 
			
		||||
	/* DNS settings */
 | 
			
		||||
	dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
 | 
			
		||||
	dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
 | 
			
		||||
	dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
 | 
			
		||||
	dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
 | 
			
		||||
	dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache');
 | 
			
		||||
	dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
 | 
			
		||||
 | 
			
		||||
	/* Routing settings */
 | 
			
		||||
	default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
 | 
			
		||||
	sniff_override = uci.get(uciconfig, uciroutingsetting, 'sniff_override');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy',
 | 
			
		||||
      ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0',
 | 
			
		||||
      default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface');
 | 
			
		||||
 | 
			
		||||
const cache_file_store_rdrc = uci.get(uciconfig, uciexp, 'cache_file_store_rdrc'),
 | 
			
		||||
      cache_file_rdrc_timeout = uci.get(uciconfig, uciexp, 'cache_file_rdrc_timeout');
 | 
			
		||||
 | 
			
		||||
const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330';
 | 
			
		||||
let self_mark, redirect_port, tproxy_port,
 | 
			
		||||
    tun_name, tun_addr4, tun_addr6, tun_mtu, tun_gso,
 | 
			
		||||
    tcpip_stack, endpoint_independent_nat;
 | 
			
		||||
if (match(proxy_mode, /redirect/)) {
 | 
			
		||||
	self_mark = uci.get(uciconfig, 'infra', 'self_mark') || '100';
 | 
			
		||||
	redirect_port = uci.get(uciconfig, 'infra', 'redirect_port') || '5331';
 | 
			
		||||
}
 | 
			
		||||
if (match(proxy_mode), /tproxy/)
 | 
			
		||||
	if (main_udp_node !== 'nil' || routing_mode === 'custom')
 | 
			
		||||
		tproxy_port = uci.get(uciconfig, 'infra', 'tproxy_port') || '5332';
 | 
			
		||||
if (match(proxy_mode), /tun/) {
 | 
			
		||||
	tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0';
 | 
			
		||||
	tun_addr4 = uci.get(uciconfig, uciinfra, 'tun_addr4') || '172.19.0.1/30';
 | 
			
		||||
	tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126';
 | 
			
		||||
	tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000';
 | 
			
		||||
	tun_gso = '0';
 | 
			
		||||
	tcpip_stack = 'system';
 | 
			
		||||
	if (routing_mode === 'custom') {
 | 
			
		||||
		tun_gso = uci.get(uciconfig, uciroutingsetting, 'tun_gso') || '0';
 | 
			
		||||
		tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system';
 | 
			
		||||
		endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
/* UCI config end */
 | 
			
		||||
 | 
			
		||||
/* Config helper start */
 | 
			
		||||
function parse_port(strport) {
 | 
			
		||||
	if (type(strport) !== 'array' || isEmpty(strport))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	let ports = [];
 | 
			
		||||
	for (let i in strport)
 | 
			
		||||
		push(ports, int(i));
 | 
			
		||||
 | 
			
		||||
	return ports;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parse_dnsquery(strquery) {
 | 
			
		||||
	if (type(strquery) !== 'array' || isEmpty(strquery))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	let querys = [];
 | 
			
		||||
	for (let i in strquery)
 | 
			
		||||
		isnan(int(i)) ? push(querys, i) : push(querys, int(i));
 | 
			
		||||
 | 
			
		||||
	return querys;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generate_outbound(node) {
 | 
			
		||||
	if (type(node) !== 'object' || isEmpty(node))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	const outbound = {
 | 
			
		||||
		type: node.type,
 | 
			
		||||
		tag: 'cfg-' + node['.name'] + '-out',
 | 
			
		||||
		routing_mark: strToInt(self_mark),
 | 
			
		||||
 | 
			
		||||
		server: node.address,
 | 
			
		||||
		server_port: strToInt(node.port),
 | 
			
		||||
 | 
			
		||||
		username: (node.type !== 'ssh') ? node.username : null,
 | 
			
		||||
		user: (node.type === 'ssh') ? node.username : null,
 | 
			
		||||
		password: node.password,
 | 
			
		||||
 | 
			
		||||
		/* Direct */
 | 
			
		||||
		override_address: node.override_address,
 | 
			
		||||
		override_port: strToInt(node.override_port),
 | 
			
		||||
		/* Hysteria (2) */
 | 
			
		||||
		up_mbps: strToInt(node.hysteria_up_mbps),
 | 
			
		||||
		down_mbps: strToInt(node.hysteria_down_mbps),
 | 
			
		||||
		obfs: node.hysteria_obfs_type ? {
 | 
			
		||||
			type: node.hysteria_obfs_type,
 | 
			
		||||
			password: node.hysteria_obfs_password
 | 
			
		||||
		} : node.hysteria_obfs_password,
 | 
			
		||||
		auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
 | 
			
		||||
		auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
 | 
			
		||||
		recv_window_conn: strToInt(node.hysteria_recv_window_conn),
 | 
			
		||||
		recv_window: strToInt(node.hysteria_revc_window),
 | 
			
		||||
		disable_mtu_discovery: strToBool(node.hysteria_disable_mtu_discovery),
 | 
			
		||||
		/* Shadowsocks */
 | 
			
		||||
		method: node.shadowsocks_encrypt_method,
 | 
			
		||||
		plugin: node.shadowsocks_plugin,
 | 
			
		||||
		plugin_opts: node.shadowsocks_plugin_opts,
 | 
			
		||||
		/* ShadowTLS / Socks */
 | 
			
		||||
		version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null),
 | 
			
		||||
		/* SSH */
 | 
			
		||||
		client_version: node.ssh_client_version,
 | 
			
		||||
		host_key: node.ssh_host_key,
 | 
			
		||||
		host_key_algorithms: node.ssh_host_key_algo,
 | 
			
		||||
		private_key: node.ssh_priv_key,
 | 
			
		||||
		private_key_passphrase: node.ssh_priv_key_pp,
 | 
			
		||||
		/* Tuic */
 | 
			
		||||
		uuid: node.uuid,
 | 
			
		||||
		congestion_control: node.tuic_congestion_control,
 | 
			
		||||
		udp_relay_mode: node.tuic_udp_relay_mode,
 | 
			
		||||
		udp_over_stream: strToBool(node.tuic_udp_over_stream),
 | 
			
		||||
		zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
 | 
			
		||||
		heartbeat: node.tuic_heartbeat ? (node.tuic_heartbeat + 's') : null,
 | 
			
		||||
		/* VLESS / VMess */
 | 
			
		||||
		flow: node.vless_flow,
 | 
			
		||||
		alter_id: strToInt(node.vmess_alterid),
 | 
			
		||||
		security: node.vmess_encrypt,
 | 
			
		||||
		global_padding: node.vmess_global_padding ? (node.vmess_global_padding === '1') : null,
 | 
			
		||||
		authenticated_length: node.vmess_authenticated_length ? (node.vmess_authenticated_length === '1') : null,
 | 
			
		||||
		packet_encoding: node.packet_encoding,
 | 
			
		||||
		/* WireGuard */
 | 
			
		||||
		system_interface: (node.type === 'wireguard') || null,
 | 
			
		||||
		gso: (node.wireguard_gso === '1') || null,
 | 
			
		||||
		interface_name: (node.type === 'wireguard') ? 'wg-' + node['.name'] + '-out' : null,
 | 
			
		||||
		local_address: node.wireguard_local_address,
 | 
			
		||||
		private_key: node.wireguard_private_key,
 | 
			
		||||
		peer_public_key: node.wireguard_peer_public_key,
 | 
			
		||||
		pre_shared_key: node.wireguard_pre_shared_key,
 | 
			
		||||
		reserved: parse_port(node.wireguard_reserved),
 | 
			
		||||
		mtu: strToInt(node.wireguard_mtu),
 | 
			
		||||
 | 
			
		||||
		multiplex: (node.multiplex === '1') ? {
 | 
			
		||||
			enabled: true,
 | 
			
		||||
			protocol: node.multiplex_protocol,
 | 
			
		||||
			max_connections: strToInt(node.multiplex_max_connections),
 | 
			
		||||
			min_streams: strToInt(node.multiplex_min_streams),
 | 
			
		||||
			max_streams: strToInt(node.multiplex_max_streams),
 | 
			
		||||
			padding: (node.multiplex_padding === '1'),
 | 
			
		||||
			brutal: (node.multiplex_brutal === '1') ? {
 | 
			
		||||
				enabled: true,
 | 
			
		||||
				up_mbps: strToInt(node.multiplex_brutal_up),
 | 
			
		||||
				down_mbps: strToInt(node.multiplex_brutal_down)
 | 
			
		||||
			} : null
 | 
			
		||||
		} : null,
 | 
			
		||||
		tls: (node.tls === '1') ? {
 | 
			
		||||
			enabled: true,
 | 
			
		||||
			server_name: node.tls_sni,
 | 
			
		||||
			insecure: (node.tls_insecure === '1'),
 | 
			
		||||
			alpn: node.tls_alpn,
 | 
			
		||||
			min_version: node.tls_min_version,
 | 
			
		||||
			max_version: node.tls_max_version,
 | 
			
		||||
			cipher_suites: node.tls_cipher_suites,
 | 
			
		||||
			certificate_path: node.tls_cert_path,
 | 
			
		||||
			ech: (node.tls_ech === '1') ? {
 | 
			
		||||
				enabled: true,
 | 
			
		||||
				dynamic_record_sizing_disabled: (node.tls_ech_tls_disable_drs === '1'),
 | 
			
		||||
				pq_signature_schemes_enabled: (node.tls_ech_enable_pqss === '1'),
 | 
			
		||||
				config: node.tls_ech_config
 | 
			
		||||
			} : null,
 | 
			
		||||
			utls: !isEmpty(node.tls_utls) ? {
 | 
			
		||||
				enabled: true,
 | 
			
		||||
				fingerprint: node.tls_utls
 | 
			
		||||
			} : null,
 | 
			
		||||
			reality: (node.tls_reality === '1') ? {
 | 
			
		||||
				enabled: true,
 | 
			
		||||
				public_key: node.tls_reality_public_key,
 | 
			
		||||
				short_id: node.tls_reality_short_id
 | 
			
		||||
			} : null
 | 
			
		||||
		} : null,
 | 
			
		||||
		transport: !isEmpty(node.transport) ? {
 | 
			
		||||
			type: node.transport,
 | 
			
		||||
			host: node.http_host || node.httpupgrade_host,
 | 
			
		||||
			path: node.http_path || node.ws_path,
 | 
			
		||||
			headers: node.ws_host ? {
 | 
			
		||||
				Host: node.ws_host
 | 
			
		||||
			} : null,
 | 
			
		||||
			method: node.http_method,
 | 
			
		||||
			max_early_data: strToInt(node.websocket_early_data),
 | 
			
		||||
			early_data_header_name: node.websocket_early_data_header,
 | 
			
		||||
			service_name: node.grpc_servicename,
 | 
			
		||||
			idle_timeout: node.http_idle_timeout ? (node.http_idle_timeout + 's') : null,
 | 
			
		||||
			ping_timeout: node.http_ping_timeout ? (node.http_ping_timeout + 's') : null,
 | 
			
		||||
			permit_without_stream: strToBool(node.grpc_permit_without_stream)
 | 
			
		||||
		} : null,
 | 
			
		||||
		udp_over_tcp: (node.udp_over_tcp === '1') ? {
 | 
			
		||||
			enabled: true,
 | 
			
		||||
			version: strToInt(node.udp_over_tcp_version)
 | 
			
		||||
		} : null,
 | 
			
		||||
		tcp_fast_open: strToBool(node.tcp_fast_open),
 | 
			
		||||
		tcp_multi_path: strToBool(node.tcp_multi_path),
 | 
			
		||||
		udp_fragment: strToBool(node.udp_fragment)
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return outbound;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_outbound(cfg) {
 | 
			
		||||
	if (isEmpty(cfg))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	if (type(cfg) === 'array') {
 | 
			
		||||
		if ('any-out' in cfg)
 | 
			
		||||
			return 'any';
 | 
			
		||||
 | 
			
		||||
		let outbounds = [];
 | 
			
		||||
		for (let i in cfg)
 | 
			
		||||
			push(outbounds, get_outbound(i));
 | 
			
		||||
		return outbounds;
 | 
			
		||||
	} else {
 | 
			
		||||
		if (cfg in ['direct-out', 'block-out']) {
 | 
			
		||||
			return cfg;
 | 
			
		||||
		} else {
 | 
			
		||||
			const node = uci.get(uciconfig, cfg, 'node');
 | 
			
		||||
			if (isEmpty(node))
 | 
			
		||||
				die(sprintf("%s's node is missing, please check your configuration.", cfg));
 | 
			
		||||
			else
 | 
			
		||||
				return 'cfg-' + node + '-out';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_resolver(cfg) {
 | 
			
		||||
	if (isEmpty(cfg))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	if (cfg in ['default-dns', 'system-dns', 'block-dns'])
 | 
			
		||||
		return cfg;
 | 
			
		||||
	else
 | 
			
		||||
		return 'cfg-' + cfg + '-dns';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_ruleset(cfg) {
 | 
			
		||||
	if (isEmpty(cfg))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	let rules = [];
 | 
			
		||||
	for (let i in cfg)
 | 
			
		||||
		push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule');
 | 
			
		||||
	return rules;
 | 
			
		||||
}
 | 
			
		||||
/* Config helper end */
 | 
			
		||||
 | 
			
		||||
const config = {};
 | 
			
		||||
 | 
			
		||||
/* Log */
 | 
			
		||||
config.log = {
 | 
			
		||||
	disabled: false,
 | 
			
		||||
	level: 'warn',
 | 
			
		||||
	output: RUN_DIR + '/sing-box-c.log',
 | 
			
		||||
	timestamp: true
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* DNS start */
 | 
			
		||||
/* Default settings */
 | 
			
		||||
config.dns = {
 | 
			
		||||
	servers: [
 | 
			
		||||
		{
 | 
			
		||||
			tag: 'default-dns',
 | 
			
		||||
			address: wan_dns,
 | 
			
		||||
			detour: 'direct-out'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			tag: 'system-dns',
 | 
			
		||||
			address: 'local',
 | 
			
		||||
			detour: 'direct-out'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			tag: 'block-dns',
 | 
			
		||||
			address: 'rcode://name_error'
 | 
			
		||||
		}
 | 
			
		||||
	],
 | 
			
		||||
	rules: [],
 | 
			
		||||
	strategy: dns_default_strategy,
 | 
			
		||||
	disable_cache: (dns_disable_cache === '1'),
 | 
			
		||||
	disable_expire: (dns_disable_cache_expire === '1'),
 | 
			
		||||
	independent_cache: (dns_independent_cache === '1'),
 | 
			
		||||
	client_subnet: dns_client_subnet
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (!isEmpty(main_node)) {
 | 
			
		||||
	/* Avoid DNS loop */
 | 
			
		||||
	const main_node_addr = uci.get(uciconfig, main_node, 'address');
 | 
			
		||||
	if (validateHostname(main_node_addr))
 | 
			
		||||
		push(config.dns.rules, {
 | 
			
		||||
			domain: main_node_addr,
 | 
			
		||||
			server: 'default-dns'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	if (dedicated_udp_node) {
 | 
			
		||||
		const main_udp_node_addr = uci.get(uciconfig, main_udp_node, 'address');
 | 
			
		||||
		if (validateHostname(main_udp_node_addr))
 | 
			
		||||
			push(config.dns.rules, {
 | 
			
		||||
				domain: main_udp_node_addr,
 | 
			
		||||
				server: 'default-dns'
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (direct_domain_list)
 | 
			
		||||
		push(config.dns.rules, {
 | 
			
		||||
			domain_keyword: direct_domain_list,
 | 
			
		||||
			server: 'default-dns'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	/* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */
 | 
			
		||||
	if (routing_mode === 'gfwlist' || proxy_domain_list)
 | 
			
		||||
		push(config.dns.rules, {
 | 
			
		||||
			domain_keyword: (routing_mode !== 'gfwlist') ? proxy_domain_list : null,
 | 
			
		||||
			query_type: [64, 65],
 | 
			
		||||
			server: 'block-dns'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	if (isEmpty(config.dns.rules))
 | 
			
		||||
		config.dns.rules = null;
 | 
			
		||||
 | 
			
		||||
	let default_final_dns = 'default-dns';
 | 
			
		||||
	/* Main DNS */
 | 
			
		||||
	if (dns_server !== wan_dns) {
 | 
			
		||||
		push(config.dns.servers, {
 | 
			
		||||
			tag: 'main-dns',
 | 
			
		||||
			address: 'tcp://' + (validation('ip6addr', dns_server) ? `[${dns_server}]` : dns_server),
 | 
			
		||||
			strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
 | 
			
		||||
			detour: 'main-out'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		default_final_dns = 'main-dns';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config.dns.final = default_final_dns;
 | 
			
		||||
} else if (!isEmpty(default_outbound)) {
 | 
			
		||||
	/* DNS servers */
 | 
			
		||||
	uci.foreach(uciconfig, ucidnsserver, (cfg) => {
 | 
			
		||||
		if (cfg.enabled !== '1')
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		push(config.dns.servers, {
 | 
			
		||||
			tag: 'cfg-' + cfg['.name'] + '-dns',
 | 
			
		||||
			address: cfg.address,
 | 
			
		||||
			address: cfg.address,
 | 
			
		||||
			address_resolver: get_resolver(cfg.address_resolver),
 | 
			
		||||
			address_strategy: cfg.address_strategy,
 | 
			
		||||
			strategy: cfg.resolve_strategy,
 | 
			
		||||
			detour: get_outbound(cfg.outbound),
 | 
			
		||||
			client_subnet: cfg.client_subnet
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	/* DNS rules */
 | 
			
		||||
	uci.foreach(uciconfig, ucidnsrule, (cfg) => {
 | 
			
		||||
		if (cfg.enabled !== '1')
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		push(config.dns.rules, {
 | 
			
		||||
			ip_version: strToInt(cfg.ip_version),
 | 
			
		||||
			query_type: parse_dnsquery(cfg.query_type),
 | 
			
		||||
			network: cfg.network,
 | 
			
		||||
			protocol: cfg.protocol,
 | 
			
		||||
			domain: cfg.domain,
 | 
			
		||||
			domain_suffix: cfg.domain_suffix,
 | 
			
		||||
			domain_keyword: cfg.domain_keyword,
 | 
			
		||||
			domain_regex: cfg.domain_regex,
 | 
			
		||||
			port: parse_port(cfg.port),
 | 
			
		||||
			port_range: cfg.port_range,
 | 
			
		||||
			source_ip_cidr: cfg.source_ip_cidr,
 | 
			
		||||
			source_ip_is_private: (cfg.source_ip_is_private === '1') || null,
 | 
			
		||||
			ip_cidr: cfg.ip_cidr,
 | 
			
		||||
			ip_is_private: (cfg.ip_is_private === '1') || null,
 | 
			
		||||
			source_port: parse_port(cfg.source_port),
 | 
			
		||||
			source_port_range: cfg.source_port_range,
 | 
			
		||||
			process_name: cfg.process_name,
 | 
			
		||||
			process_path: cfg.process_path,
 | 
			
		||||
			user: cfg.user,
 | 
			
		||||
			rule_set: get_ruleset(cfg.rule_set),
 | 
			
		||||
			rule_set_ipcidr_match_source: (cfg.rule_set_ipcidr_match_source === '1') || null,
 | 
			
		||||
			invert: (cfg.invert === '1') || null,
 | 
			
		||||
			outbound: get_outbound(cfg.outbound),
 | 
			
		||||
			server: get_resolver(cfg.server),
 | 
			
		||||
			disable_cache: (cfg.dns_disable_cache === '1') || null,
 | 
			
		||||
			rewrite_ttl: strToInt(cfg.rewrite_ttl),
 | 
			
		||||
			client_subnet: cfg.client_subnet
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (isEmpty(config.dns.rules))
 | 
			
		||||
		config.dns.rules = null;
 | 
			
		||||
 | 
			
		||||
	config.dns.final = get_resolver(dns_default_server);
 | 
			
		||||
}
 | 
			
		||||
/* DNS end */
 | 
			
		||||
 | 
			
		||||
/* Inbound start */
 | 
			
		||||
config.inbounds = [];
 | 
			
		||||
 | 
			
		||||
push(config.inbounds, {
 | 
			
		||||
	type: 'direct',
 | 
			
		||||
	tag: 'dns-in',
 | 
			
		||||
	listen: '::',
 | 
			
		||||
	listen_port: int(dns_port)
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
push(config.inbounds, {
 | 
			
		||||
	type: 'mixed',
 | 
			
		||||
	tag: 'mixed-in',
 | 
			
		||||
	listen: '::',
 | 
			
		||||
	listen_port: int(mixed_port),
 | 
			
		||||
	sniff: true,
 | 
			
		||||
	sniff_override_destination: (sniff_override === '1'),
 | 
			
		||||
	set_system_proxy: false
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (match(proxy_mode, /redirect/))
 | 
			
		||||
	push(config.inbounds, {
 | 
			
		||||
		type: 'redirect',
 | 
			
		||||
		tag: 'redirect-in',
 | 
			
		||||
 | 
			
		||||
		listen: '::',
 | 
			
		||||
		listen_port: int(redirect_port),
 | 
			
		||||
		sniff: true,
 | 
			
		||||
		sniff_override_destination: (sniff_override === '1')
 | 
			
		||||
	});
 | 
			
		||||
if (match(proxy_mode, /tproxy/))
 | 
			
		||||
	push(config.inbounds, {
 | 
			
		||||
		type: 'tproxy',
 | 
			
		||||
		tag: 'tproxy-in',
 | 
			
		||||
 | 
			
		||||
		listen: '::',
 | 
			
		||||
		listen_port: int(tproxy_port),
 | 
			
		||||
		network: 'udp',
 | 
			
		||||
		sniff: true,
 | 
			
		||||
		sniff_override_destination: (sniff_override === '1')
 | 
			
		||||
	});
 | 
			
		||||
if (match(proxy_mode, /tun/))
 | 
			
		||||
	push(config.inbounds, {
 | 
			
		||||
		type: 'tun',
 | 
			
		||||
		tag: 'tun-in',
 | 
			
		||||
 | 
			
		||||
		interface_name: tun_name,
 | 
			
		||||
		inet4_address: tun_addr4,
 | 
			
		||||
		inet6_address: (ipv6_support === '1') ? tun_addr6 : null,
 | 
			
		||||
		mtu: strToInt(tun_mtu),
 | 
			
		||||
		gso: (tun_gso === '1'),
 | 
			
		||||
		auto_route: false,
 | 
			
		||||
		endpoint_independent_nat: strToBool(endpoint_independent_nat),
 | 
			
		||||
		stack: tcpip_stack,
 | 
			
		||||
		sniff: true,
 | 
			
		||||
		sniff_override_destination: (sniff_override === '1'),
 | 
			
		||||
	});
 | 
			
		||||
/* Inbound end */
 | 
			
		||||
 | 
			
		||||
/* Outbound start */
 | 
			
		||||
/* Default outbounds */
 | 
			
		||||
config.outbounds = [
 | 
			
		||||
	{
 | 
			
		||||
		type: 'direct',
 | 
			
		||||
		tag: 'direct-out',
 | 
			
		||||
		routing_mark: strToInt(self_mark)
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'block',
 | 
			
		||||
		tag: 'block-out'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'dns',
 | 
			
		||||
		tag: 'dns-out'
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/* Main outbounds */
 | 
			
		||||
if (!isEmpty(main_node)) {
 | 
			
		||||
	const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
 | 
			
		||||
	push(config.outbounds, generate_outbound(main_node_cfg));
 | 
			
		||||
	config.outbounds[length(config.outbounds)-1].tag = 'main-out';
 | 
			
		||||
 | 
			
		||||
	if (dedicated_udp_node) {
 | 
			
		||||
		const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
 | 
			
		||||
		push(config.outbounds, generate_outbound(main_udp_node_cfg));
 | 
			
		||||
		config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
 | 
			
		||||
	}
 | 
			
		||||
} else if (!isEmpty(default_outbound))
 | 
			
		||||
	uci.foreach(uciconfig, uciroutingnode, (cfg) => {
 | 
			
		||||
		if (cfg.enabled !== '1')
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		const outbound = uci.get_all(uciconfig, cfg.node) || {};
 | 
			
		||||
		push(config.outbounds, generate_outbound(outbound));
 | 
			
		||||
		config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy;
 | 
			
		||||
		config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
 | 
			
		||||
		config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
 | 
			
		||||
	});
 | 
			
		||||
/* Outbound end */
 | 
			
		||||
 | 
			
		||||
/* Routing rules start */
 | 
			
		||||
/* Default settings */
 | 
			
		||||
config.route = {
 | 
			
		||||
	rules: [
 | 
			
		||||
		{
 | 
			
		||||
			inbound: 'dns-in',
 | 
			
		||||
			outbound: 'dns-out'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			protocol: 'dns',
 | 
			
		||||
			outbound: 'dns-out'
 | 
			
		||||
		}
 | 
			
		||||
	],
 | 
			
		||||
	rule_set: [],
 | 
			
		||||
	auto_detect_interface: isEmpty(default_interface) ? true : null,
 | 
			
		||||
	default_interface: default_interface
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* Routing rules */
 | 
			
		||||
if (!isEmpty(main_node)) {
 | 
			
		||||
	/* Direct list */
 | 
			
		||||
	if (length(direct_domain_list))
 | 
			
		||||
		push(config.route.rules, {
 | 
			
		||||
			domain_keyword: direct_domain_list,
 | 
			
		||||
			outbound: 'direct-out'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	/* Main UDP out */
 | 
			
		||||
	if (dedicated_udp_node)
 | 
			
		||||
		push(config.route.rules, {
 | 
			
		||||
			network: 'udp',
 | 
			
		||||
			outbound: 'main-udp-out'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	config.route.final = 'main-out';
 | 
			
		||||
} else if (!isEmpty(default_outbound)) {
 | 
			
		||||
	uci.foreach(uciconfig, uciroutingrule, (cfg) => {
 | 
			
		||||
		if (cfg.enabled !== '1')
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		push(config.route.rules, {
 | 
			
		||||
			ip_version: strToInt(cfg.ip_version),
 | 
			
		||||
			protocol: cfg.protocol,
 | 
			
		||||
			network: cfg.network,
 | 
			
		||||
			domain: cfg.domain,
 | 
			
		||||
			domain_suffix: cfg.domain_suffix,
 | 
			
		||||
			domain_keyword: cfg.domain_keyword,
 | 
			
		||||
			domain_regex: cfg.domain_regex,
 | 
			
		||||
			source_ip_cidr: cfg.source_ip_cidr,
 | 
			
		||||
			source_ip_is_private: (cfg.source_ip_is_private === '1') || null,
 | 
			
		||||
			ip_cidr: cfg.ip_cidr,
 | 
			
		||||
			ip_is_private: (cfg.ip_is_private === '1') || null,
 | 
			
		||||
			source_port: parse_port(cfg.source_port),
 | 
			
		||||
			source_port_range: cfg.source_port_range,
 | 
			
		||||
			port: parse_port(cfg.port),
 | 
			
		||||
			port_range: cfg.port_range,
 | 
			
		||||
			process_name: cfg.process_name,
 | 
			
		||||
			process_path: cfg.process_path,
 | 
			
		||||
			user: cfg.user,
 | 
			
		||||
			rule_set: get_ruleset(cfg.rule_set),
 | 
			
		||||
			rule_set_ipcidr_match_source: (cfg.rule_set_ipcidr_match_source === '1') || null,
 | 
			
		||||
			invert: (cfg.invert === '1') || null,
 | 
			
		||||
			outbound: get_outbound(cfg.outbound)
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	config.route.final = get_outbound(default_outbound);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* Rule set */
 | 
			
		||||
if (routing_mode === 'custom') {
 | 
			
		||||
	uci.foreach(uciconfig, uciruleset, (cfg) => {
 | 
			
		||||
		if (cfg.enabled !== '1')
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		push(config.route.rule_set, {
 | 
			
		||||
			type: cfg.type,
 | 
			
		||||
			tag: 'cfg-' + cfg['.name'] + '-rule',
 | 
			
		||||
			format: cfg.format,
 | 
			
		||||
			path: cfg.path,
 | 
			
		||||
			url: cfg.url,
 | 
			
		||||
			download_detour: get_outbound(cfg.outbound),
 | 
			
		||||
			update_interval: cfg.update_interval
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
/* Routing rules end */
 | 
			
		||||
 | 
			
		||||
/* Experimental start */
 | 
			
		||||
if (routing_mode === 'custom') {
 | 
			
		||||
	config.experimental = {
 | 
			
		||||
		cache_file: {
 | 
			
		||||
			enabled: true,
 | 
			
		||||
			path: HP_DIR + '/cache.db',
 | 
			
		||||
			store_rdrc: (cache_file_store_rdrc === '1') || null,
 | 
			
		||||
			rdrc_timeout: cache_file_rdrc_timeout
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
/* Experimental end */
 | 
			
		||||
 | 
			
		||||
system('mkdir -p ' + RUN_DIR);
 | 
			
		||||
writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config)));
 | 
			
		||||
@ -1,175 +0,0 @@
 | 
			
		||||
#!/usr/bin/ucode
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
import { readfile, writefile } from 'fs';
 | 
			
		||||
import { cursor } from 'uci';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
	executeCommand, isEmpty, strToBool, strToInt,
 | 
			
		||||
	removeBlankAttrs, validateHostname, validation,
 | 
			
		||||
	HP_DIR, RUN_DIR
 | 
			
		||||
} from 'homeproxy';
 | 
			
		||||
 | 
			
		||||
/* UCI config start */
 | 
			
		||||
const uci = cursor();
 | 
			
		||||
 | 
			
		||||
const uciconfig = 'homeproxy';
 | 
			
		||||
uci.load(uciconfig);
 | 
			
		||||
 | 
			
		||||
const uciserver = 'server';
 | 
			
		||||
 | 
			
		||||
const config = {};
 | 
			
		||||
 | 
			
		||||
/* Log */
 | 
			
		||||
config.log = {
 | 
			
		||||
	disabled: false,
 | 
			
		||||
	level: 'warn',
 | 
			
		||||
	output: RUN_DIR + '/sing-box-s.log',
 | 
			
		||||
	timestamp: true
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
config.inbounds = [];
 | 
			
		||||
 | 
			
		||||
uci.foreach(uciconfig, uciserver, (cfg) => {
 | 
			
		||||
	if (cfg.enabled !== '1')
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	push(config.inbounds, {
 | 
			
		||||
		type: cfg.type,
 | 
			
		||||
		tag: 'cfg-' + cfg['.name'] + '-in',
 | 
			
		||||
 | 
			
		||||
		listen: cfg.address || '::',
 | 
			
		||||
		listen_port: strToInt(cfg.port),
 | 
			
		||||
		tcp_fast_open: strToBool(cfg.tcp_fast_open),
 | 
			
		||||
		tcp_multi_path: strToBool(cfg.tcp_multi_path),
 | 
			
		||||
		udp_fragment: strToBool(cfg.udp_fragment),
 | 
			
		||||
		sniff: true,
 | 
			
		||||
		sniff_override_destination: (cfg.sniff_override === '1'),
 | 
			
		||||
		domain_strategy: cfg.domain_strategy,
 | 
			
		||||
		network: cfg.network,
 | 
			
		||||
 | 
			
		||||
		/* Hysteria */
 | 
			
		||||
		up_mbps: strToInt(cfg.hysteria_up_mbps),
 | 
			
		||||
		down_mbps: strToInt(cfg.hysteria_down_mbps),
 | 
			
		||||
		obfs: cfg.hysteria_obfs_type ? {
 | 
			
		||||
			type: cfg.hysteria_obfs_type,
 | 
			
		||||
			password: cfg.hysteria_obfs_password
 | 
			
		||||
		} : cfg.hysteria_obfs_password,
 | 
			
		||||
		recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
 | 
			
		||||
		recv_window_client: strToInt(cfg.hysteria_revc_window_client),
 | 
			
		||||
		max_conn_client: strToInt(cfg.hysteria_max_conn_client),
 | 
			
		||||
		disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
 | 
			
		||||
		ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
 | 
			
		||||
		masquerade: cfg.hysteria_masquerade,
 | 
			
		||||
 | 
			
		||||
		/* Shadowsocks */
 | 
			
		||||
		method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
 | 
			
		||||
		password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
 | 
			
		||||
 | 
			
		||||
		/* Tuic */
 | 
			
		||||
		congestion_control: cfg.tuic_congestion_control,
 | 
			
		||||
		auth_timeout: cfg.tuic_auth_timeout ? (cfg.tuic_auth_timeout + 's') : null,
 | 
			
		||||
		zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
 | 
			
		||||
		heartbeat: cfg.tuic_heartbeat ? (cfg.tuic_heartbeat + 's') : null,
 | 
			
		||||
 | 
			
		||||
		/* HTTP / Hysteria (2) / Socks / Trojan / Tuic / VLESS / VMess */
 | 
			
		||||
		users: (cfg.type !== 'shadowsocks') ? [
 | 
			
		||||
			{
 | 
			
		||||
				name: !(cfg.type in ['http', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
 | 
			
		||||
				username: cfg.username,
 | 
			
		||||
				password: cfg.password,
 | 
			
		||||
 | 
			
		||||
				/* Hysteria */
 | 
			
		||||
				auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
 | 
			
		||||
				auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
 | 
			
		||||
 | 
			
		||||
				/* Tuic */
 | 
			
		||||
				uuid: cfg.uuid,
 | 
			
		||||
 | 
			
		||||
				/* VLESS / VMess */
 | 
			
		||||
				flow: cfg.vless_flow,
 | 
			
		||||
				alterId: strToInt(cfg.vmess_alterid)
 | 
			
		||||
			}
 | 
			
		||||
		] : null,
 | 
			
		||||
 | 
			
		||||
		multiplex: (cfg.multiplex === '1') ? {
 | 
			
		||||
			enabled: true,
 | 
			
		||||
			padding: (cfg.multiplex_padding === '1'),
 | 
			
		||||
			brutal: (cfg.multiplex_brutal === '1') ? {
 | 
			
		||||
				enabled: true,
 | 
			
		||||
				up_mbps: strToInt(cfg.multiplex_brutal_up),
 | 
			
		||||
				down_mbps: strToInt(cfg.multiplex_brutal_down)
 | 
			
		||||
			} : null
 | 
			
		||||
		} : null,
 | 
			
		||||
 | 
			
		||||
		tls: (cfg.tls === '1') ? {
 | 
			
		||||
			enabled: true,
 | 
			
		||||
			server_name: cfg.tls_sni,
 | 
			
		||||
			alpn: cfg.tls_alpn,
 | 
			
		||||
			min_version: cfg.tls_min_version,
 | 
			
		||||
			max_version: cfg.tls_max_version,
 | 
			
		||||
			cipher_suites: cfg.tls_cipher_suites,
 | 
			
		||||
			certificate_path: cfg.tls_cert_path,
 | 
			
		||||
			key_path: cfg.tls_key_path,
 | 
			
		||||
			acme: (cfg.tls_acme === '1') ? {
 | 
			
		||||
				domain: cfg.tls_acme_domains,
 | 
			
		||||
				data_directory: HP_DIR + '/certs',
 | 
			
		||||
				default_server_name: cfg.tls_acme_dsn,
 | 
			
		||||
				email: cfg.tls_acme_email,
 | 
			
		||||
				provider: cfg.tls_acme_provider,
 | 
			
		||||
				disable_http_challenge: (cfg.tls_acme_dhc === '1'),
 | 
			
		||||
				disable_tls_alpn_challenge: (cfg.tls_acme_dtac === '1'),
 | 
			
		||||
				alternative_http_port: strToInt(cfg.tls_acme_ahp),
 | 
			
		||||
				alternative_tls_port: strToInt(cfg.tls_acme_atp),
 | 
			
		||||
				external_account: (cfg.tls_acme_external_account === '1') ? {
 | 
			
		||||
					key_id: cfg.tls_acme_ea_keyid,
 | 
			
		||||
					mac_key: cfg.tls_acme_ea_mackey
 | 
			
		||||
				} : null,
 | 
			
		||||
				dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
 | 
			
		||||
					provider: cfg.tls_dns01_provider,
 | 
			
		||||
					access_key_id: cfg.tls_dns01_ali_akid,
 | 
			
		||||
					access_key_secret: cfg.tls_dns01_ali_aksec,
 | 
			
		||||
					region_id: cfg.tls_dns01_ali_rid,
 | 
			
		||||
					api_token: cfg.tls_dns01_cf_api_token
 | 
			
		||||
				} : null
 | 
			
		||||
			} : null,
 | 
			
		||||
			reality: (cfg.tls_reality === '1') ? {
 | 
			
		||||
				enabled: true,
 | 
			
		||||
				private_key: cfg.tls_reality_private_key,
 | 
			
		||||
				short_id: cfg.tls_reality_short_id,
 | 
			
		||||
				max_time_difference: cfg.tls_reality_max_time_difference ? (cfg.max_time_difference + 's') : null,
 | 
			
		||||
				handshake: {
 | 
			
		||||
					server: cfg.tls_reality_server_addr,
 | 
			
		||||
					server_port: strToInt(cfg.tls_reality_server_port)
 | 
			
		||||
					}
 | 
			
		||||
			} : null
 | 
			
		||||
		} : null,
 | 
			
		||||
 | 
			
		||||
		transport: !isEmpty(cfg.transport) ? {
 | 
			
		||||
			type: cfg.transport,
 | 
			
		||||
			host: cfg.http_host || cfg.httpupgrade_host,
 | 
			
		||||
			path: cfg.http_path || cfg.ws_path,
 | 
			
		||||
			headers: cfg.ws_host ? {
 | 
			
		||||
				Host: cfg.ws_host
 | 
			
		||||
			} : null,
 | 
			
		||||
			method: cfg.http_method,
 | 
			
		||||
			max_early_data: strToInt(cfg.websocket_early_data),
 | 
			
		||||
			early_data_header_name: cfg.websocket_early_data_header,
 | 
			
		||||
			service_name: cfg.grpc_servicename,
 | 
			
		||||
			idle_timeout: cfg.http_idle_timeout ? (cfg.http_idle_timeout + 's') : null,
 | 
			
		||||
			ping_timeout: cfg.http_ping_timeout ? (cfg.http_ping_timeout + 's') : null
 | 
			
		||||
		} : null
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (length(config.inbounds) === 0)
 | 
			
		||||
	exit(1);
 | 
			
		||||
 | 
			
		||||
system('mkdir -p ' + RUN_DIR);
 | 
			
		||||
writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config)));
 | 
			
		||||
@ -1,231 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { mkstemp } from 'fs';
 | 
			
		||||
import { urldecode, urldecode_params } from 'luci.http';
 | 
			
		||||
 | 
			
		||||
/* Global variables start */
 | 
			
		||||
export const HP_DIR = '/etc/homeproxy';
 | 
			
		||||
export const RUN_DIR = '/var/run/homeproxy';
 | 
			
		||||
/* Global variables end */
 | 
			
		||||
 | 
			
		||||
/* Utilities start */
 | 
			
		||||
/* Kanged from luci-app-commands */
 | 
			
		||||
export function shellQuote(s) {
 | 
			
		||||
	return `'${replace(s, "'", "'\\''")}'`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function isBinary(str) {
 | 
			
		||||
	for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
 | 
			
		||||
		if (byte <= 8 || (byte >= 14 && byte <= 31))
 | 
			
		||||
			return true;
 | 
			
		||||
 | 
			
		||||
	return false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function executeCommand(...args) {
 | 
			
		||||
	let outfd = mkstemp();
 | 
			
		||||
	let errfd = mkstemp();
 | 
			
		||||
 | 
			
		||||
	const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
 | 
			
		||||
 | 
			
		||||
	outfd.seek(0);
 | 
			
		||||
	errfd.seek(0);
 | 
			
		||||
 | 
			
		||||
	const stdout = outfd.read(1024 * 512) ?? '';
 | 
			
		||||
	const stderr = errfd.read(1024 * 512) ?? '';
 | 
			
		||||
 | 
			
		||||
	outfd.close();
 | 
			
		||||
	errfd.close();
 | 
			
		||||
 | 
			
		||||
	const binary = isBinary(stdout);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		command: join(' ', args),
 | 
			
		||||
		stdout: binary ? null : stdout,
 | 
			
		||||
		stderr,
 | 
			
		||||
		exitcode,
 | 
			
		||||
		binary
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function calcStringMD5(str) {
 | 
			
		||||
	if (!str || type(str) !== 'string')
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	const output = executeCommand(`/bin/echo -n ${shellQuote(str)} | /usr/bin/md5sum | /usr/bin/awk '{print $1}'`) || {};
 | 
			
		||||
	return trim(output.stdout);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getTime(epoch) {
 | 
			
		||||
	const local_time = localtime(epoch);
 | 
			
		||||
	return replace(replace(sprintf(
 | 
			
		||||
		'%d-%2d-%2d@%2d:%2d:%2d',
 | 
			
		||||
		local_time.year,
 | 
			
		||||
		local_time.mon,
 | 
			
		||||
		local_time.mday,
 | 
			
		||||
		local_time.hour,
 | 
			
		||||
		local_time.min,
 | 
			
		||||
		local_time.sec
 | 
			
		||||
	), ' ', '0'), '@', ' ');
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function wGET(url) {
 | 
			
		||||
	if (!url || type(url) !== 'string')
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	const output = executeCommand(`/usr/bin/wget -qO- --user-agent 'Wget/1.21 (HomeProxy, like v2rayN)' --timeout=10 ${shellQuote(url)}`) || {};
 | 
			
		||||
	return trim(output.stdout);
 | 
			
		||||
};
 | 
			
		||||
/* Utilities end */
 | 
			
		||||
 | 
			
		||||
/* String helper start */
 | 
			
		||||
export function isEmpty(res) {
 | 
			
		||||
	return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function strToBool(str) {
 | 
			
		||||
	return (str === '1') || null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function strToInt(str) {
 | 
			
		||||
	return !isEmpty(str) ? (int(str) || null) : null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function removeBlankAttrs(res) {
 | 
			
		||||
	let content;
 | 
			
		||||
 | 
			
		||||
	if (type(res) === 'object') {
 | 
			
		||||
		content = {};
 | 
			
		||||
		map(keys(res), (k) => {
 | 
			
		||||
			if (type(res[k]) in ['array', 'object'])
 | 
			
		||||
				content[k] = removeBlankAttrs(res[k]);
 | 
			
		||||
			else if (res[k] !== null && res[k] !== '')
 | 
			
		||||
				content[k] = res[k];
 | 
			
		||||
		});
 | 
			
		||||
	} else if (type(res) === 'array') {
 | 
			
		||||
		content = [];
 | 
			
		||||
		map(res, (k, i) => {
 | 
			
		||||
			if (type(k) in ['array', 'object'])
 | 
			
		||||
				push(content, removeBlankAttrs(k));
 | 
			
		||||
			else if (k !== null && k !== '')
 | 
			
		||||
				push(content, k);
 | 
			
		||||
		});
 | 
			
		||||
	} else
 | 
			
		||||
		return res;
 | 
			
		||||
 | 
			
		||||
	return content;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function validateHostname(hostname) {
 | 
			
		||||
	return (match(hostname, /^[a-zA-Z0-9_]+$/) != null ||
 | 
			
		||||
		(match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) &&
 | 
			
		||||
			match(hostname, /[^0-9.]/)));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function validation(datatype, data) {
 | 
			
		||||
	if (!datatype || !data)
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
 | 
			
		||||
	return (ret === 0);
 | 
			
		||||
};
 | 
			
		||||
/* String helper end */
 | 
			
		||||
 | 
			
		||||
/* String parser start */
 | 
			
		||||
export function decodeBase64Str(str) {
 | 
			
		||||
	if (isEmpty(str))
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	str = trim(str);
 | 
			
		||||
	str = replace(str, '_', '/');
 | 
			
		||||
	str = replace(str, '-', '+');
 | 
			
		||||
 | 
			
		||||
	const padding = length(str) % 4;
 | 
			
		||||
	if (padding)
 | 
			
		||||
		str = str + substr('====', padding);
 | 
			
		||||
 | 
			
		||||
	return b64dec(str);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function parseURL(url) {
 | 
			
		||||
	if (type(url) !== 'string')
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	const services = {
 | 
			
		||||
		http: '80',
 | 
			
		||||
		https: '443'
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const objurl = {};
 | 
			
		||||
 | 
			
		||||
	objurl.href = url;
 | 
			
		||||
 | 
			
		||||
	url = replace(url, /#(.+)$/, (_, val) => {
 | 
			
		||||
		objurl.hash = val;
 | 
			
		||||
		return '';
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => {
 | 
			
		||||
		objurl.protocol = val;
 | 
			
		||||
		return '';
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	url = replace(url, /\?(.+)/, (_, val) => {
 | 
			
		||||
		objurl.search = val;
 | 
			
		||||
		objurl.searchParams = urldecode_params(val);
 | 
			
		||||
		return '';
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	url = replace(url, /^\/\/([^\/]+)/, (_, val) => {
 | 
			
		||||
		val = replace(val, /^([^@]+)@/, (_, val) => {
 | 
			
		||||
			objurl.userinfo = val;
 | 
			
		||||
			return '';
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		val = replace(val, /:(\d+)$/, (_, val) => {
 | 
			
		||||
			objurl.port = val;
 | 
			
		||||
			return '';
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (validation('ip4addr', val) ||
 | 
			
		||||
		    validation('ip6addr', replace(val, /\[|\]/g, '')) ||
 | 
			
		||||
		    validation('hostname', val))
 | 
			
		||||
			objurl.hostname = val;
 | 
			
		||||
 | 
			
		||||
		return '';
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	objurl.pathname = url || '/';
 | 
			
		||||
 | 
			
		||||
	if (!objurl.protocol || !objurl.hostname)
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	if (objurl.userinfo) {
 | 
			
		||||
		objurl.userinfo = replace(objurl.userinfo, /:([^:]+)$/, (_, val) => {
 | 
			
		||||
			objurl.password = val;
 | 
			
		||||
			return '';
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) {
 | 
			
		||||
			objurl.username = objurl.userinfo;
 | 
			
		||||
			delete objurl.userinfo;
 | 
			
		||||
		} else {
 | 
			
		||||
			delete objurl.userinfo;
 | 
			
		||||
			delete objurl.password;
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	if (!objurl.port)
 | 
			
		||||
		objurl.port = services[objurl.protocol];
 | 
			
		||||
 | 
			
		||||
	objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : '');
 | 
			
		||||
	objurl.origin = `${objurl.protocol}://${objurl.host}`;
 | 
			
		||||
 | 
			
		||||
	return objurl;
 | 
			
		||||
};
 | 
			
		||||
/* String parser end */
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
# SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2023 ImmortalWrt.org
 | 
			
		||||
 | 
			
		||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
 | 
			
		||||
 | 
			
		||||
for i in "china_ip4" "china_ip6" "gfw_list" "china_list"; do
 | 
			
		||||
	"$SCRIPTS_DIR"/update_resources.sh "$i"
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
"$SCRIPTS_DIR"/update_subscriptions.uc
 | 
			
		||||
@ -1,105 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
# SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 | 
			
		||||
NAME="homeproxy"
 | 
			
		||||
 | 
			
		||||
RESOURCES_DIR="/etc/$NAME/resources"
 | 
			
		||||
mkdir -p "$RESOURCES_DIR"
 | 
			
		||||
 | 
			
		||||
RUN_DIR="/var/run/$NAME"
 | 
			
		||||
LOG_PATH="$RUN_DIR/$NAME.log"
 | 
			
		||||
mkdir -p "$RUN_DIR"
 | 
			
		||||
 | 
			
		||||
log() {
 | 
			
		||||
	echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
set_lock() {
 | 
			
		||||
	local act="$1"
 | 
			
		||||
	local type="$2"
 | 
			
		||||
 | 
			
		||||
	local lock="$RUN_DIR/update_resources-$type.lock"
 | 
			
		||||
	if [ "$act" = "set" ]; then
 | 
			
		||||
		if [ -e "$lock" ]; then
 | 
			
		||||
			log "[$(to_upper "$type")] A task is already running."
 | 
			
		||||
			exit 2
 | 
			
		||||
		else
 | 
			
		||||
			touch "$lock"
 | 
			
		||||
		fi
 | 
			
		||||
	elif [ "$act" = "remove" ]; then
 | 
			
		||||
		rm -f "$lock"
 | 
			
		||||
	fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
to_upper() {
 | 
			
		||||
	echo -e "$1" | tr "[a-z]" "[A-Z]"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
check_list_update() {
 | 
			
		||||
	local listtype="$1"
 | 
			
		||||
	local listrepo="$2"
 | 
			
		||||
	local listref="$3"
 | 
			
		||||
	local listname="$4"
 | 
			
		||||
	local wget="wget --timeout=10 -q"
 | 
			
		||||
 | 
			
		||||
	set_lock "set" "$listtype"
 | 
			
		||||
 | 
			
		||||
	local list_info="$($wget -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname")"
 | 
			
		||||
	local list_sha="$(echo -e "$list_info" | jsonfilter -e "@[0].sha")"
 | 
			
		||||
	local list_ver="$(echo -e "$list_info" | jsonfilter -e "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')"
 | 
			
		||||
	if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then
 | 
			
		||||
		log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later."
 | 
			
		||||
 | 
			
		||||
		set_lock "remove" "$listtype"
 | 
			
		||||
		return 1
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")"
 | 
			
		||||
	if [ "$local_list_ver" = "$list_ver" ]; then
 | 
			
		||||
		log "[$(to_upper "$listtype")] Current version: $list_ver."
 | 
			
		||||
		log "[$(to_upper "$listtype")] You're already at the latest version."
 | 
			
		||||
 | 
			
		||||
		set_lock "remove" "$listtype"
 | 
			
		||||
		return 3
 | 
			
		||||
	else
 | 
			
		||||
		log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver."
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	$wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname"
 | 
			
		||||
	if [ ! -s "$RUN_DIR/$listname" ]; then
 | 
			
		||||
		rm -f "$RUN_DIR/$listname"
 | 
			
		||||
		log "[$(to_upper "$listtype")] Update failed."
 | 
			
		||||
 | 
			
		||||
		set_lock "remove" "$listtype"
 | 
			
		||||
		return 1
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"
 | 
			
		||||
	echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
 | 
			
		||||
	log "[$(to_upper "$listtype")] Successfully updated."
 | 
			
		||||
 | 
			
		||||
	set_lock "remove" "$listtype"
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
case "$1" in
 | 
			
		||||
"china_ip4")
 | 
			
		||||
	check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
 | 
			
		||||
	;;
 | 
			
		||||
"china_ip6")
 | 
			
		||||
	check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
 | 
			
		||||
	;;
 | 
			
		||||
"gfw_list")
 | 
			
		||||
	check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
 | 
			
		||||
	;;
 | 
			
		||||
"china_list")
 | 
			
		||||
	check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
 | 
			
		||||
		sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
 | 
			
		||||
	;;
 | 
			
		||||
*)
 | 
			
		||||
	echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
 | 
			
		||||
	exit 1
 | 
			
		||||
	;;
 | 
			
		||||
esac
 | 
			
		||||
@ -1,617 +0,0 @@
 | 
			
		||||
#!/usr/bin/ucode
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
import { open } from 'fs';
 | 
			
		||||
import { connect } from 'ubus';
 | 
			
		||||
import { cursor } from 'uci';
 | 
			
		||||
 | 
			
		||||
import { urldecode, urlencode, urldecode_params } from 'luci.http';
 | 
			
		||||
import { init_action } from 'luci.sys';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
	calcStringMD5, wGET, executeCommand, decodeBase64Str,
 | 
			
		||||
	getTime, isEmpty, parseURL, validation,
 | 
			
		||||
	HP_DIR, RUN_DIR
 | 
			
		||||
} from 'homeproxy';
 | 
			
		||||
 | 
			
		||||
/* UCI config start */
 | 
			
		||||
const uci = cursor();
 | 
			
		||||
 | 
			
		||||
const uciconfig = 'homeproxy';
 | 
			
		||||
uci.load(uciconfig);
 | 
			
		||||
 | 
			
		||||
const ucimain = 'config',
 | 
			
		||||
      ucinode = 'node',
 | 
			
		||||
      ucisubscription = 'subscription';
 | 
			
		||||
 | 
			
		||||
const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '0',
 | 
			
		||||
      filter_mode = uci.get(uciconfig, ucisubscription, 'filter_nodes') || 'disabled',
 | 
			
		||||
      filter_keywords = uci.get(uciconfig, ucisubscription, 'filter_keywords') || [],
 | 
			
		||||
      packet_encoding = uci.get(uciconfig, ucisubscription, 'packet_encoding') || 'xudp',
 | 
			
		||||
      subscription_urls = uci.get(uciconfig, ucisubscription, 'subscription_url') || [],
 | 
			
		||||
      via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0';
 | 
			
		||||
 | 
			
		||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainalnd_china';
 | 
			
		||||
let main_node, main_udp_node;
 | 
			
		||||
if (routing_mode !== 'custom') {
 | 
			
		||||
	main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
 | 
			
		||||
	main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
 | 
			
		||||
}
 | 
			
		||||
/* UCI config end */
 | 
			
		||||
 | 
			
		||||
/* String helper start */
 | 
			
		||||
function filter_check(name) {
 | 
			
		||||
	if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	let ret = false;
 | 
			
		||||
	for (let i in filter_keywords) {
 | 
			
		||||
		const patten = regexp(i);
 | 
			
		||||
		if (match(name, patten))
 | 
			
		||||
			ret = true;
 | 
			
		||||
	}
 | 
			
		||||
	if (filter_mode === 'whitelist')
 | 
			
		||||
		ret = !ret;
 | 
			
		||||
 | 
			
		||||
	return ret;
 | 
			
		||||
}
 | 
			
		||||
/* String helper end */
 | 
			
		||||
 | 
			
		||||
/* Common var start */
 | 
			
		||||
const node_cache = {},
 | 
			
		||||
      node_result = [];
 | 
			
		||||
 | 
			
		||||
const ubus = connect();
 | 
			
		||||
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
 | 
			
		||||
/* Common var end */
 | 
			
		||||
 | 
			
		||||
/* Log */
 | 
			
		||||
system(`mkdir -p ${RUN_DIR}`);
 | 
			
		||||
function log(...args) {
 | 
			
		||||
	const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
 | 
			
		||||
	logfile.write(`${getTime()} [SUBSCRIBE] ${join(' ', args)}\n`);
 | 
			
		||||
	logfile.close();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parse_uri(uri) {
 | 
			
		||||
	let config, url, params;
 | 
			
		||||
 | 
			
		||||
	if (type(uri) === 'object') {
 | 
			
		||||
		if (uri.nodetype === 'sip008') {
 | 
			
		||||
			/* https://shadowsocks.org/guide/sip008.html */
 | 
			
		||||
			config = {
 | 
			
		||||
				label: uri.remarks,
 | 
			
		||||
				type: 'shadowsocks',
 | 
			
		||||
				address: uri.server,
 | 
			
		||||
				port: uri.server_port,
 | 
			
		||||
				shadowsocks_encrypt_method: uri.method,
 | 
			
		||||
				password: uri.password,
 | 
			
		||||
				shadowsocks_plugin: uri.plugin,
 | 
			
		||||
				shadowsocks_plugin_opts: uri.plugin_opts
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	} else if (type(uri) === 'string') {
 | 
			
		||||
		uri = split(trim(uri), '://');
 | 
			
		||||
 | 
			
		||||
		switch (uri[0]) {
 | 
			
		||||
		case 'http':
 | 
			
		||||
		case 'https':
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'http',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				username: url.username ? urldecode(url.username) : null,
 | 
			
		||||
				password: url.password ? urldecode(url.password) : null,
 | 
			
		||||
				tls: (uri[0] === 'https') ? '1' : '0'
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'hysteria':
 | 
			
		||||
			/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
			params = url.searchParams;
 | 
			
		||||
 | 
			
		||||
			if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s node: %s.', 'hysteria', urldecode(url.hash) || url.hostname));
 | 
			
		||||
				if (!sing_features.with_quic)
 | 
			
		||||
					log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
 | 
			
		||||
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'hysteria',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				hysteria_protocol: params.protocol || 'udp',
 | 
			
		||||
				hysteria_auth_type: params.auth ? 'string' : null,
 | 
			
		||||
				hysteria_auth_payload: params.auth,
 | 
			
		||||
				hysteria_obfs_password: params.obfsParam,
 | 
			
		||||
				hysteria_down_mbps: params.downmbps,
 | 
			
		||||
				hysteria_up_mbps: params.upmbps,
 | 
			
		||||
				tls: '1',
 | 
			
		||||
				tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
 | 
			
		||||
				tls_sni: params.peer,
 | 
			
		||||
				tls_alpn: params.alpn
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'hysteria2':
 | 
			
		||||
		case 'hy2':
 | 
			
		||||
			/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
			params = url.searchParams;
 | 
			
		||||
 | 
			
		||||
			if (!sing_features.with_quic) {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s node: %s.', 'hysteria2', urldecode(url.hash) || url.hostname));
 | 
			
		||||
				log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'hysteria2',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				password: url.username ? (
 | 
			
		||||
					urldecode(url.username + (url.password ? (':' + url.password) : ''))
 | 
			
		||||
				) : null,
 | 
			
		||||
				hysteria_obfs_type: params.obfs,
 | 
			
		||||
				hysteria_obfs_password: params['obfs-password'],
 | 
			
		||||
				tls: '1',
 | 
			
		||||
				tls_insecure: params.insecure ? '1' : '0',
 | 
			
		||||
				tls_sni: params.sni
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'socks':
 | 
			
		||||
		case 'socks4':
 | 
			
		||||
		case 'socks4a':
 | 
			
		||||
		case 'socsk5':
 | 
			
		||||
		case 'socks5h':
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'socks',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				username: url.username ? urldecode(url.username) : null,
 | 
			
		||||
				password: url.password ? urldecode(url.password) : null,
 | 
			
		||||
				socks_version: (match(uri[0], /4/)) ? '4' : '5'
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'ss':
 | 
			
		||||
			/* "Lovely" Shadowrocket format */
 | 
			
		||||
			const ss_suri = split(uri[1], '#');
 | 
			
		||||
			let ss_slabel = '';
 | 
			
		||||
			if (length(ss_suri) <= 2) {
 | 
			
		||||
				if (length(ss_suri) === 2)
 | 
			
		||||
					ss_slabel = '#' + urlencode(ss_suri[1]);
 | 
			
		||||
				if (decodeBase64Str(ss_suri[0]))
 | 
			
		||||
					uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/* Legacy format is not supported, it should be never appeared in modern subscriptions */
 | 
			
		||||
			/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
 | 
			
		||||
 | 
			
		||||
			/* SIP002 format https://shadowsocks.org/guide/sip002.html */
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
 | 
			
		||||
			let ss_userinfo = {};
 | 
			
		||||
			if (url.username && url.password)
 | 
			
		||||
				/* User info encoded with URIComponent */
 | 
			
		||||
				ss_userinfo = [url.username, urldecode(url.password)];
 | 
			
		||||
			else if (url.username)
 | 
			
		||||
				/* User info encoded with base64 */
 | 
			
		||||
				ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':');
 | 
			
		||||
 | 
			
		||||
			let ss_plugin, ss_plugin_opts;
 | 
			
		||||
			if (url.search && url.searchParams.plugin) {
 | 
			
		||||
				const ss_plugin_info = split(url.searchParams.plugin, ';');
 | 
			
		||||
				ss_plugin = ss_plugin_info[0];
 | 
			
		||||
				if (ss_plugin === 'simple-obfs')
 | 
			
		||||
					/* Fix non-standard plugin name */
 | 
			
		||||
					ss_plugin = 'obfs-local';
 | 
			
		||||
				ss_plugin_opts = slice(ss_plugin_info, 1) ? join(';', slice(ss_plugin_info, 1)) : null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'shadowsocks',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				shadowsocks_encrypt_method: ss_userinfo[0],
 | 
			
		||||
				password: ss_userinfo[1],
 | 
			
		||||
				shadowsocks_plugin: ss_plugin,
 | 
			
		||||
				shadowsocks_plugin_opts: ss_plugin_opts
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'trojan':
 | 
			
		||||
			/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
			params = url.searchParams || {};
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'trojan',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				password: urldecode(url.username),
 | 
			
		||||
				transport: (params.type !== 'tcp') ? params.type : null,
 | 
			
		||||
				tls: '1',
 | 
			
		||||
				tls_sni: params.sni
 | 
			
		||||
			};
 | 
			
		||||
			switch(params.type) {
 | 
			
		||||
			case 'grpc':
 | 
			
		||||
				config.grpc_servicename = params.serviceName;
 | 
			
		||||
				break;
 | 
			
		||||
			case 'ws':
 | 
			
		||||
				config.ws_host = params.host ? urldecode(params.host) : null;
 | 
			
		||||
				config.ws_path = params.path ? urldecode(params.path) : null;
 | 
			
		||||
				if (config.ws_path && match(config.ws_path, /\?ed=/)) {
 | 
			
		||||
					config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
 | 
			
		||||
					config.websocket_early_data = split(config.ws_path, '?ed=')[1];
 | 
			
		||||
					config.ws_path = split(config.ws_path, '?ed=')[0];
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'tuic':
 | 
			
		||||
			/* https://github.com/daeuniverse/dae/discussions/182 */
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
			params = url.searchParams || {};
 | 
			
		||||
 | 
			
		||||
			if (!sing_features.with_quic) {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s node: %s.', 'TUIC', urldecode(url.hash) || url.hostname));
 | 
			
		||||
				log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
 | 
			
		||||
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'tuic',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				uuid: url.username,
 | 
			
		||||
				password: url.password ? urldecode(url.password) : null,
 | 
			
		||||
				tuic_congestion_control: params.congestion_control,
 | 
			
		||||
				tuic_udp_relay_mode: params.udp_relay_mode,
 | 
			
		||||
				tls: '1',
 | 
			
		||||
				tls_sni: params.sni,
 | 
			
		||||
				tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'vless':
 | 
			
		||||
			/* https://github.com/XTLS/Xray-core/discussions/716 */
 | 
			
		||||
			url = parseURL('http://' + uri[1]);
 | 
			
		||||
			params = url.searchParams;
 | 
			
		||||
 | 
			
		||||
			/* Unsupported protocol */
 | 
			
		||||
			if (params.type === 'kcp') {
 | 
			
		||||
				log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(url.hash) || url.hostname));
 | 
			
		||||
				return null;
 | 
			
		||||
			} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) {
 | 
			
		||||
				log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(url.hash) || url.hostname));
 | 
			
		||||
				if (!sing_features.with_quic)
 | 
			
		||||
					log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
 | 
			
		||||
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: url.hash ? urldecode(url.hash) : null,
 | 
			
		||||
				type: 'vless',
 | 
			
		||||
				address: url.hostname,
 | 
			
		||||
				port: url.port,
 | 
			
		||||
				uuid: url.username,
 | 
			
		||||
				transport: (params.type !== 'tcp') ? params.type : null,
 | 
			
		||||
				tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
 | 
			
		||||
				tls_sni: params.sni,
 | 
			
		||||
				tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
 | 
			
		||||
				tls_reality: (params.security === 'reality') ? '1' : '0',
 | 
			
		||||
				tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
 | 
			
		||||
				tls_reality_short_id: params.sid,
 | 
			
		||||
				tls_utls: sing_features.with_utls ? params.fp : null,
 | 
			
		||||
				vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
 | 
			
		||||
			};
 | 
			
		||||
			switch(params.type) {
 | 
			
		||||
			case 'grpc':
 | 
			
		||||
				config.grpc_servicename = params.serviceName;
 | 
			
		||||
				break;
 | 
			
		||||
			case 'http':
 | 
			
		||||
			case 'tcp':
 | 
			
		||||
				if (params.type === 'http' || params.headerType === 'http') {
 | 
			
		||||
					config.http_host = params.host ? split(urldecode(params.host), ',') : null;
 | 
			
		||||
					config.http_path = params.path ? urldecode(params.path) : null;
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'ws':
 | 
			
		||||
				config.ws_host = params.host ? urldecode(params.host) : null;
 | 
			
		||||
				config.ws_path = params.path ? urldecode(params.path) : null;
 | 
			
		||||
				if (config.ws_path && match(config.ws_path, /\?ed=/)) {
 | 
			
		||||
					config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
 | 
			
		||||
					config.websocket_early_data = split(config.ws_path, '?ed=')[1];
 | 
			
		||||
					config.ws_path = split(config.ws_path, '?ed=')[0];
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		case 'vmess':
 | 
			
		||||
			/* "Lovely" shadowrocket format */
 | 
			
		||||
			if (match(uri, /&/)) {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s format.', 'VMess'));
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/* https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) */
 | 
			
		||||
			try {
 | 
			
		||||
				uri = json(decodeBase64Str(uri[1]));
 | 
			
		||||
			} catch(e) {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s format.', 'VMess'));
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (uri.v != '2') {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s format.', 'VMess'));
 | 
			
		||||
				return null;
 | 
			
		||||
			/* Unsupported protocol */
 | 
			
		||||
			} else if (uri.net === 'kcp') {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
 | 
			
		||||
				return null;
 | 
			
		||||
			} else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) {
 | 
			
		||||
				log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
 | 
			
		||||
				if (!sing_features.with_quic)
 | 
			
		||||
					log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
 | 
			
		||||
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
			/*
 | 
			
		||||
			 * https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
 | 
			
		||||
			 * else if (uri.aid && int(uri.aid) !== 0) {
 | 
			
		||||
			 * 	log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
 | 
			
		||||
			 * 	return null;
 | 
			
		||||
			 * }
 | 
			
		||||
			 */
 | 
			
		||||
 | 
			
		||||
			config = {
 | 
			
		||||
				label: uri.ps ? urldecode(uri.ps) : null,
 | 
			
		||||
				type: 'vmess',
 | 
			
		||||
				address: uri.add,
 | 
			
		||||
				port: uri.port,
 | 
			
		||||
				uuid: uri.id,
 | 
			
		||||
				vmess_alterid: uri.aid,
 | 
			
		||||
				vmess_encrypt: uri.scy || 'auto',
 | 
			
		||||
				vmess_global_padding: '1',
 | 
			
		||||
				transport: (uri.net !== 'tcp') ? uri.net : null,
 | 
			
		||||
				tls: (uri.tls === 'tls') ? '1' : '0',
 | 
			
		||||
				tls_sni: uri.sni || uri.host,
 | 
			
		||||
				tls_alpn: uri.alpn ? split(uri.alpn, ',') : null
 | 
			
		||||
			};
 | 
			
		||||
			switch (uri.net) {
 | 
			
		||||
			case 'grpc':
 | 
			
		||||
				config.grpc_servicename = uri.path;
 | 
			
		||||
				break;
 | 
			
		||||
			case 'h2':
 | 
			
		||||
			case 'tcp':
 | 
			
		||||
				if (uri.net === 'h2' || uri.type === 'http') {
 | 
			
		||||
					config.transport = 'http';
 | 
			
		||||
					config.http_host = uri.host ? uri.host.split(',') : null;
 | 
			
		||||
					config.http_path = uri.path;
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'ws':
 | 
			
		||||
				config.ws_host = uri.host;
 | 
			
		||||
				config.ws_path = uri.path;
 | 
			
		||||
				if (config.ws_path && match(config.ws_path, /\?ed=/)) {
 | 
			
		||||
					config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
 | 
			
		||||
					config.websocket_early_data = split(config.ws_path, '?ed=')[1];
 | 
			
		||||
					config.ws_path = split(config.ws_path, '?ed=')[0];
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!isEmpty(config)) {
 | 
			
		||||
		if (config.address)
 | 
			
		||||
			config.address = replace(config.address, /\[|\]/g, '');
 | 
			
		||||
 | 
			
		||||
		if (!validation('host', config.address) || !validation('port', config.port)) {
 | 
			
		||||
			log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
 | 
			
		||||
			return null;
 | 
			
		||||
		} else if (!config.label)
 | 
			
		||||
			config.label = (validation('ip6addr', config.address) ?
 | 
			
		||||
				`[${config.address}]` : config.address) + ':' + config.port;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return config;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function main() {
 | 
			
		||||
	if (via_proxy !== '1') {
 | 
			
		||||
		log('Stopping service...');
 | 
			
		||||
		init_action('homeproxy', 'stop');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for (let url in subscription_urls) {
 | 
			
		||||
		url = replace(url, /#.*$/, '');
 | 
			
		||||
		const groupHash = calcStringMD5(url);
 | 
			
		||||
		node_cache[groupHash] = {};
 | 
			
		||||
 | 
			
		||||
		const res = wGET(url);
 | 
			
		||||
		if (isEmpty(res)) {
 | 
			
		||||
			log(sprintf('Failed to fetch resources from %s.', url));
 | 
			
		||||
			continue;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let nodes;
 | 
			
		||||
		try {
 | 
			
		||||
			nodes = json(res).servers || json(res);
 | 
			
		||||
 | 
			
		||||
			/* Shadowsocks SIP008 format */
 | 
			
		||||
			if (nodes[0].server && nodes[0].method)
 | 
			
		||||
				map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
 | 
			
		||||
		} catch(e) {
 | 
			
		||||
			nodes = decodeBase64Str(res);
 | 
			
		||||
			nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : {};
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let count = 0;
 | 
			
		||||
		for (let node in nodes) {
 | 
			
		||||
			let config;
 | 
			
		||||
			if (!isEmpty(node))
 | 
			
		||||
				config = parse_uri(node);
 | 
			
		||||
			if (isEmpty(config))
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			const label = config.label;
 | 
			
		||||
			config.label = null;
 | 
			
		||||
			const confHash = calcStringMD5(sprintf('%J', config)),
 | 
			
		||||
			      nameHash = calcStringMD5(label);
 | 
			
		||||
			config.label = label;
 | 
			
		||||
 | 
			
		||||
			if (filter_check(config.label))
 | 
			
		||||
				log(sprintf('Skipping blacklist node: %s.', config.label));
 | 
			
		||||
			else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash])
 | 
			
		||||
				log(sprintf('Skipping duplicate node: %s.', config.label));
 | 
			
		||||
			else {
 | 
			
		||||
				if (config.tls === '1' && allow_insecure === '1')
 | 
			
		||||
					config.tls_insecure = '1';
 | 
			
		||||
				if (config.type in ['vless', 'vmess'])
 | 
			
		||||
					config.packet_encoding = packet_encoding;
 | 
			
		||||
 | 
			
		||||
				config.grouphash = groupHash;
 | 
			
		||||
				push(node_result, []);
 | 
			
		||||
				push(node_result[length(node_result)-1], config);
 | 
			
		||||
				node_cache[groupHash][confHash] = config;
 | 
			
		||||
				node_cache[groupHash][nameHash] = config;
 | 
			
		||||
 | 
			
		||||
				count++;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (count == 0)
 | 
			
		||||
			log(sprintf('No valid node found in %s.', url));
 | 
			
		||||
		else
 | 
			
		||||
			log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (isEmpty(node_result)) {
 | 
			
		||||
		log('Failed to update subscriptions: no valid node found.');
 | 
			
		||||
 | 
			
		||||
		if (via_proxy !== '1') {
 | 
			
		||||
			log('Starting service...');
 | 
			
		||||
			init_action('homeproxy', 'start');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let added = 0, removed = 0;
 | 
			
		||||
	uci.foreach(uciconfig, ucinode, (cfg) => {
 | 
			
		||||
		/* Nodes created by the user */
 | 
			
		||||
		if (!cfg.grouphash)
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		/* Empty object - failed to fetch nodes */
 | 
			
		||||
		if (length(node_cache[cfg.grouphash]) === 0)
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) {
 | 
			
		||||
			uci.delete(uciconfig, cfg['.name']);
 | 
			
		||||
			removed++;
 | 
			
		||||
 | 
			
		||||
			log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
 | 
			
		||||
		} else {
 | 
			
		||||
			map(keys(node_cache[cfg.grouphash][cfg['.name']]), (v) => {
 | 
			
		||||
				uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
 | 
			
		||||
			});
 | 
			
		||||
			node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	for (let nodes in node_result)
 | 
			
		||||
		map(nodes, (node) => {
 | 
			
		||||
			if (node.isExisting)
 | 
			
		||||
				return null;
 | 
			
		||||
 | 
			
		||||
			const nameHash = calcStringMD5(node.label);
 | 
			
		||||
			uci.set(uciconfig, nameHash, 'node');
 | 
			
		||||
			map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v]));
 | 
			
		||||
 | 
			
		||||
			added++;
 | 
			
		||||
			log(sprintf('Adding node: %s.', node.label));
 | 
			
		||||
		});
 | 
			
		||||
	uci.commit(uciconfig);
 | 
			
		||||
 | 
			
		||||
	let need_restart = (via_proxy !== '1');
 | 
			
		||||
	if (!isEmpty(main_node)) {
 | 
			
		||||
		const first_server = uci.get_first(uciconfig, ucinode);
 | 
			
		||||
		if (first_server) {
 | 
			
		||||
			if (!uci.get(uciconfig, main_node)) {
 | 
			
		||||
				uci.set(uciconfig, ucimain, 'main_node', first_server);
 | 
			
		||||
				uci.commit(uciconfig);
 | 
			
		||||
				need_restart = true;
 | 
			
		||||
 | 
			
		||||
				log('Main node is gone, switching to the first node.');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
 | 
			
		||||
				if (!uci.get(uciconfig, main_udp_node)) {
 | 
			
		||||
					uci.set(uciconfig, ucimain, 'main_udp_node', first_server);
 | 
			
		||||
					uci.commit(uciconfig);
 | 
			
		||||
					need_restart = true;
 | 
			
		||||
 | 
			
		||||
					log('Main UDP node is gone, switching to the first node.');
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			uci.set(uciconfig, ucimain, 'main_node', 'nil');
 | 
			
		||||
			uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
 | 
			
		||||
			uci.commit(uciconfig);
 | 
			
		||||
			need_restart = true;
 | 
			
		||||
 | 
			
		||||
			log('No available node, disable tproxy.');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (need_restart) {
 | 
			
		||||
		log('Restarting service...');
 | 
			
		||||
		init_action('homeproxy', 'stop');
 | 
			
		||||
		init_action('homeproxy', 'start');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log(sprintf('%s nodes added, %s removed.', added, removed));
 | 
			
		||||
	log('Successfully updated subscriptions.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!isEmpty(subscription_urls))
 | 
			
		||||
	try {
 | 
			
		||||
		call(main);
 | 
			
		||||
	} catch(e) {
 | 
			
		||||
		log('[FATAL ERROR] An error occurred during updating subscriptions:');
 | 
			
		||||
		log(sprintf('%s: %s', e.type, e.message));
 | 
			
		||||
		log(e.stacktrace[0].context);
 | 
			
		||||
 | 
			
		||||
		log('Restarting service...');
 | 
			
		||||
		init_action('homeproxy', 'stop');
 | 
			
		||||
		init_action('homeproxy', 'start');
 | 
			
		||||
	}
 | 
			
		||||
@ -1,380 +0,0 @@
 | 
			
		||||
#!/bin/sh /etc/rc.common
 | 
			
		||||
# SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2022-2023 ImmortalWrt.org
 | 
			
		||||
 | 
			
		||||
USE_PROCD=1
 | 
			
		||||
 | 
			
		||||
START=99
 | 
			
		||||
STOP=10
 | 
			
		||||
 | 
			
		||||
CONF="homeproxy"
 | 
			
		||||
PROG="/usr/bin/sing-box"
 | 
			
		||||
 | 
			
		||||
HP_DIR="/etc/homeproxy"
 | 
			
		||||
RUN_DIR="/var/run/homeproxy"
 | 
			
		||||
LOG_PATH="$RUN_DIR/homeproxy.log"
 | 
			
		||||
DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
 | 
			
		||||
 | 
			
		||||
log() {
 | 
			
		||||
	echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
start_service() {
 | 
			
		||||
	config_load "$CONF"
 | 
			
		||||
 | 
			
		||||
	local routing_mode proxy_mode
 | 
			
		||||
	config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
 | 
			
		||||
	config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
 | 
			
		||||
 | 
			
		||||
	local outbound_node
 | 
			
		||||
	if [ "$routing_mode" != "custom" ]; then
 | 
			
		||||
		config_get outbound_node "config" "main_node" "nil"
 | 
			
		||||
	else
 | 
			
		||||
		config_get outbound_node "routing" "default_outbound" "nil"
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	local server_enabled
 | 
			
		||||
	config_get_bool server_enabled "server" "enabled" "0"
 | 
			
		||||
 | 
			
		||||
	if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then
 | 
			
		||||
		return 1
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	mkdir -p "$RUN_DIR"
 | 
			
		||||
 | 
			
		||||
	if [ "$outbound_node" != "nil" ]; then
 | 
			
		||||
		# Generate/Validate client config
 | 
			
		||||
		ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"
 | 
			
		||||
 | 
			
		||||
		if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then
 | 
			
		||||
			log "Error: failed to generate client configuration."
 | 
			
		||||
			return 1
 | 
			
		||||
		elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then
 | 
			
		||||
			log "Error: wrong client configuration detected."
 | 
			
		||||
			return 1
 | 
			
		||||
		fi
 | 
			
		||||
 | 
			
		||||
		# Auto update
 | 
			
		||||
		local auto_update auto_update_time
 | 
			
		||||
		config_get_bool auto_update "subscription" "auto_update" "0"
 | 
			
		||||
		if [ "$auto_update" = "1" ]; then
 | 
			
		||||
			config_get auto_update_time "subscription" "auto_update_time" "2"
 | 
			
		||||
			echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh" >> "/etc/crontabs/root"
 | 
			
		||||
			/etc/init.d/cron restart
 | 
			
		||||
		fi
 | 
			
		||||
 | 
			
		||||
		# DNSMasq rules
 | 
			
		||||
		local ipv6_support
 | 
			
		||||
		config_get_bool ipv6_support "config" "ipv6_support" "0"
 | 
			
		||||
		local dns_port china_dns_server china_dns_port
 | 
			
		||||
		config_get dns_port "infra" "dns_port" "5333"
 | 
			
		||||
		mkdir -p "$DNSMASQ_DIR"
 | 
			
		||||
		echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
 | 
			
		||||
		case "$routing_mode" in
 | 
			
		||||
		"gfwlist")
 | 
			
		||||
			[ "$ipv6_support" -eq "0" ] || local gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6"
 | 
			
		||||
			sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$gfw_nftset_v6/g" \
 | 
			
		||||
				"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
 | 
			
		||||
			;;
 | 
			
		||||
		"bypass_mainland_china")
 | 
			
		||||
			config_get china_dns_server "config" "china_dns_server"
 | 
			
		||||
			config_get china_dns_port "infra" "china_dns_port" "5334"
 | 
			
		||||
 | 
			
		||||
			if [ -e "/usr/bin/chinadns-ng" ] && [ -n "$china_dns_server" ]; then
 | 
			
		||||
				cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
 | 
			
		||||
					no-poll
 | 
			
		||||
					no-resolv
 | 
			
		||||
					server=127.0.0.1#$china_dns_port
 | 
			
		||||
				EOF
 | 
			
		||||
			else
 | 
			
		||||
				china_dns_server=""
 | 
			
		||||
				sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
 | 
			
		||||
					"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
 | 
			
		||||
			fi
 | 
			
		||||
			;;
 | 
			
		||||
		"proxy_mainland_china")
 | 
			
		||||
			sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
 | 
			
		||||
				"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
 | 
			
		||||
			;;
 | 
			
		||||
		"custom"|"global")
 | 
			
		||||
			cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
 | 
			
		||||
				no-poll
 | 
			
		||||
				no-resolv
 | 
			
		||||
				server=127.0.0.1#$dns_port
 | 
			
		||||
			EOF
 | 
			
		||||
			;;
 | 
			
		||||
		esac
 | 
			
		||||
 | 
			
		||||
		if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then
 | 
			
		||||
			[ "$ipv6_support" -eq "0" ] || local wan_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6"
 | 
			
		||||
			sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \
 | 
			
		||||
				"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
 | 
			
		||||
		fi
 | 
			
		||||
		/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
 | 
			
		||||
 | 
			
		||||
		# Setup routing table
 | 
			
		||||
		local table_mark
 | 
			
		||||
		config_get table_mark "infra" "table_mark" "100"
 | 
			
		||||
		case "$proxy_mode" in
 | 
			
		||||
		"redirect_tproxy")
 | 
			
		||||
			local outbound_udp_node
 | 
			
		||||
			config_get outbound_udp_node "config" "main_udp_node" "nil"
 | 
			
		||||
			if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then
 | 
			
		||||
				local tproxy_mark
 | 
			
		||||
				config_get tproxy_mark "infra" "tproxy_mark" "101"
 | 
			
		||||
 | 
			
		||||
				ip rule add fwmark "$tproxy_mark" table "$table_mark"
 | 
			
		||||
				ip route add local 0.0.0.0/0 dev lo table "$table_mark"
 | 
			
		||||
 | 
			
		||||
				if [ "$ipv6_support" -eq "1" ]; then
 | 
			
		||||
					ip -6 rule add fwmark "$tproxy_mark" table "$table_mark"
 | 
			
		||||
					ip -6 route add local ::/0 dev lo table "$table_mark"
 | 
			
		||||
				fi
 | 
			
		||||
			fi
 | 
			
		||||
			;;
 | 
			
		||||
		"redirect_tun"|"tun")
 | 
			
		||||
			local tun_name tun_mark
 | 
			
		||||
			config_get tun_name "infra" "tun_name" "singtun0"
 | 
			
		||||
			config_get tun_mark "infra" "tun_mark" "102"
 | 
			
		||||
 | 
			
		||||
			ip tuntap add mode tun user root name "$tun_name"
 | 
			
		||||
			sleep 1s
 | 
			
		||||
			ip link set "$tun_name" up
 | 
			
		||||
 | 
			
		||||
			ip route replace default dev "$tun_name" table "$table_mark"
 | 
			
		||||
			ip rule add fwmark "$tun_mark" lookup "$table_mark"
 | 
			
		||||
 | 
			
		||||
			ip -6 route replace default dev "$tun_name" table "$table_mark"
 | 
			
		||||
			ip -6 rule add fwmark "$tun_mark" lookup "$table_mark"
 | 
			
		||||
			;;
 | 
			
		||||
		esac
 | 
			
		||||
 | 
			
		||||
		# sing-box (client)
 | 
			
		||||
		procd_open_instance "sing-box-c"
 | 
			
		||||
 | 
			
		||||
		procd_set_param command "$PROG"
 | 
			
		||||
		procd_append_param command run --config "$RUN_DIR/sing-box-c.json"
 | 
			
		||||
 | 
			
		||||
		if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then
 | 
			
		||||
			procd_add_jail "sing-box-c" log procfs
 | 
			
		||||
			procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
 | 
			
		||||
			procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
 | 
			
		||||
			procd_add_jail_mount "$HP_DIR/certs/"
 | 
			
		||||
			procd_add_jail_mount "/etc/ssl/"
 | 
			
		||||
			procd_add_jail_mount "/etc/localtime"
 | 
			
		||||
			procd_add_jail_mount "/etc/TZ"
 | 
			
		||||
			procd_set_param capabilities "/etc/capabilities/homeproxy.json"
 | 
			
		||||
			procd_set_param no_new_privs 1
 | 
			
		||||
			procd_set_param user sing-box
 | 
			
		||||
			procd_set_param group sing-box
 | 
			
		||||
		fi
 | 
			
		||||
 | 
			
		||||
		procd_set_param limits core="unlimited"
 | 
			
		||||
		procd_set_param limits nofile="1000000 1000000"
 | 
			
		||||
		procd_set_param stderr 1
 | 
			
		||||
		procd_set_param respawn
 | 
			
		||||
 | 
			
		||||
		procd_close_instance
 | 
			
		||||
 | 
			
		||||
		# chinadns-ng
 | 
			
		||||
		if [ -n "$china_dns_server" ]; then
 | 
			
		||||
			local wandns="$(ifstatus wan | jsonfilter -e '@["dns-server"][0]' || echo "119.29.29.29")"
 | 
			
		||||
			china_dns_server="${china_dns_server/wan/$wandns}"
 | 
			
		||||
			china_dns_server="${china_dns_server// /,}"
 | 
			
		||||
 | 
			
		||||
			for i in $(seq 1 "$(grep -c "processor" "/proc/cpuinfo")"); do
 | 
			
		||||
				procd_open_instance "chinadns-ng-$i"
 | 
			
		||||
 | 
			
		||||
				procd_set_param command "/usr/bin/chinadns-ng"
 | 
			
		||||
				procd_append_param command --bind-port "$china_dns_port"
 | 
			
		||||
				procd_append_param command --china-dns "$china_dns_server"
 | 
			
		||||
				procd_append_param command --trust-dns "127.0.0.1#$dns_port"
 | 
			
		||||
				procd_append_param command --ipset-name4 "inet@fw4@homeproxy_mainland_addr_v4"
 | 
			
		||||
				procd_append_param command --ipset-name6 "inet@fw4@homeproxy_mainland_addr_v6"
 | 
			
		||||
				procd_append_param command --chnlist-file "$HP_DIR/resources/china_list.txt"
 | 
			
		||||
				procd_append_param command --gfwlist-file "$HP_DIR/resources/gfw_list.txt"
 | 
			
		||||
				procd_append_param command --reuse-port
 | 
			
		||||
 | 
			
		||||
				if chinadns-ng --version | grep -q "target:"; then
 | 
			
		||||
					procd_append_param command --cache 10000
 | 
			
		||||
					procd_append_param command --cache-stale 3600
 | 
			
		||||
					procd_append_param command --verdict-cache 10000
 | 
			
		||||
					[ "$ipv6_support" -eq "1" ] || procd_append_param command --no-ipv6=ip:non_china
 | 
			
		||||
				else
 | 
			
		||||
					[ "$ipv6_support" -eq "1" ] || procd_append_param command --no-ipv6=tC
 | 
			
		||||
				fi
 | 
			
		||||
 | 
			
		||||
				if [ -x "/sbin/ujail" ]; then
 | 
			
		||||
					procd_add_jail "chinadns-ng" log
 | 
			
		||||
					procd_add_jail_mount "$HP_DIR/resources/china_list.txt"
 | 
			
		||||
					procd_add_jail_mount "$HP_DIR/resources/gfw_list.txt"
 | 
			
		||||
					procd_set_param capabilities "/etc/capabilities/homeproxy.json"
 | 
			
		||||
					procd_set_param no_new_privs 1
 | 
			
		||||
					procd_set_param user sing-box
 | 
			
		||||
					procd_set_param group sing-box
 | 
			
		||||
				fi
 | 
			
		||||
 | 
			
		||||
				procd_set_param limits core="unlimited"
 | 
			
		||||
				procd_set_param limits nofile="1000000 1000000"
 | 
			
		||||
				procd_set_param stderr 1
 | 
			
		||||
				procd_set_param respawn
 | 
			
		||||
 | 
			
		||||
				procd_close_instance
 | 
			
		||||
			done
 | 
			
		||||
		fi
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	if [ "$server_enabled" = "1" ]; then
 | 
			
		||||
		# Generate/Validate server config
 | 
			
		||||
		ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"
 | 
			
		||||
 | 
			
		||||
		if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then
 | 
			
		||||
			log "Error: failed to generate server configuration."
 | 
			
		||||
			return 1
 | 
			
		||||
		elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then
 | 
			
		||||
			log "Error: wrong server configuration detected."
 | 
			
		||||
			return 1
 | 
			
		||||
		fi
 | 
			
		||||
 | 
			
		||||
		# sing-box (server)
 | 
			
		||||
		procd_open_instance "sing-box-s"
 | 
			
		||||
 | 
			
		||||
		procd_set_param command "$PROG"
 | 
			
		||||
		procd_append_param command run --config "$RUN_DIR/sing-box-s.json"
 | 
			
		||||
 | 
			
		||||
		if [ -x "/sbin/ujail" ]; then
 | 
			
		||||
			procd_add_jail "sing-box-s" log procfs
 | 
			
		||||
			procd_add_jail_mount "$RUN_DIR/sing-box-s.json"
 | 
			
		||||
			procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log"
 | 
			
		||||
			procd_add_jail_mount "$HP_DIR/certs/"
 | 
			
		||||
			procd_add_jail_mount "/etc/localtime"
 | 
			
		||||
			procd_add_jail_mount "/etc/TZ"
 | 
			
		||||
			procd_set_param capabilities "/etc/capabilities/homeproxy.json"
 | 
			
		||||
			procd_set_param no_new_privs 1
 | 
			
		||||
			procd_set_param user sing-box
 | 
			
		||||
			procd_set_param group sing-box
 | 
			
		||||
		fi
 | 
			
		||||
 | 
			
		||||
		procd_set_param limits core="unlimited"
 | 
			
		||||
		procd_set_param limits nofile="1000000 1000000"
 | 
			
		||||
		procd_set_param stderr 1
 | 
			
		||||
		procd_set_param respawn
 | 
			
		||||
 | 
			
		||||
		procd_close_instance
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	# log-cleaner
 | 
			
		||||
	procd_open_instance "log-cleaner"
 | 
			
		||||
	procd_set_param command "$HP_DIR/scripts/clean_log.sh"
 | 
			
		||||
	procd_set_param respawn
 | 
			
		||||
	procd_close_instance
 | 
			
		||||
 | 
			
		||||
	# Prepare ruleset directory for custom routing mode
 | 
			
		||||
	if [ "$routing_mode" = "custom" ]; then
 | 
			
		||||
		[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	# Update permissions for ujail
 | 
			
		||||
	if [ "$outbound_node" != "nil" ]; then
 | 
			
		||||
		echo > "$RUN_DIR/sing-box-c.log"
 | 
			
		||||
		chown sing-box:sing-box "$RUN_DIR/sing-box-c.log"
 | 
			
		||||
		chown sing-box:sing-box "$RUN_DIR/sing-box-c.json"
 | 
			
		||||
		chmod 0644 "$HP_DIR/resources/gfw_list.txt"
 | 
			
		||||
	fi
 | 
			
		||||
	if [ "$server_enabled" = "1" ]; then
 | 
			
		||||
		echo > "$RUN_DIR/sing-box-s.log"
 | 
			
		||||
		chown sing-box:sing-box "$RUN_DIR/sing-box-s.log"
 | 
			
		||||
		chown sing-box:sing-box "$RUN_DIR/sing-box-s.json"
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
	# Setup firewall
 | 
			
		||||
	utpl -S "$HP_DIR/scripts/firewall_pre.ut" > "$RUN_DIR/fw4_pre.nft"
 | 
			
		||||
	[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
 | 
			
		||||
	fw4 reload >"/dev/null" 2>&1
 | 
			
		||||
 | 
			
		||||
	log "$(sing-box version | awk 'NR==1{print $1,$3}') started."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stop_service() {
 | 
			
		||||
	sed -i "/$CONF/d" "/etc/crontabs/root" 2>"/dev/null"
 | 
			
		||||
	/etc/init.d/cron restart >"/dev/null" 2>&1
 | 
			
		||||
 | 
			
		||||
	# Setup firewall
 | 
			
		||||
	# Load config
 | 
			
		||||
	config_load "$CONF"
 | 
			
		||||
	local table_mark tproxy_mark tun_mark tun_name
 | 
			
		||||
	config_get table_mark "infra" "table_mark" "100"
 | 
			
		||||
	config_get tproxy_mark "infra" "tproxy_mark" "101"
 | 
			
		||||
	config_get tun_mark "infra" "tun_mark" "102"
 | 
			
		||||
	config_get tun_name "infra" "tun_name" "singtun0"
 | 
			
		||||
 | 
			
		||||
	# Tproxy
 | 
			
		||||
	ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
 | 
			
		||||
	ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null"
 | 
			
		||||
	ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
 | 
			
		||||
	ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"
 | 
			
		||||
 | 
			
		||||
	# TUN
 | 
			
		||||
	ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
 | 
			
		||||
	ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
 | 
			
		||||
 | 
			
		||||
	ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
 | 
			
		||||
	ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
 | 
			
		||||
 | 
			
		||||
	# Nftables rules
 | 
			
		||||
	for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \
 | 
			
		||||
		 "homeproxy_redirect" "homeproxy_redirect_proxy" \
 | 
			
		||||
		 "homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \
 | 
			
		||||
		 "homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
 | 
			
		||||
		 "homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \
 | 
			
		||||
		 "homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \
 | 
			
		||||
		 "homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do
 | 
			
		||||
		nft flush chain inet fw4 "$i"
 | 
			
		||||
		nft delete chain inet fw4 "$i"
 | 
			
		||||
	done 2>"/dev/null"
 | 
			
		||||
	for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \
 | 
			
		||||
		 "homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \
 | 
			
		||||
		 "homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \
 | 
			
		||||
		 "homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \
 | 
			
		||||
		 "homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \
 | 
			
		||||
		 "homeproxy_routing_port"; do
 | 
			
		||||
		nft flush set inet fw4 "$i"
 | 
			
		||||
		nft delete set inet fw4 "$i"
 | 
			
		||||
	done 2>"/dev/null"
 | 
			
		||||
	echo > "$RUN_DIR/fw4_pre.nft" 2>"/dev/null"
 | 
			
		||||
	echo > "$RUN_DIR/fw4_post.nft" 2>"/dev/null"
 | 
			
		||||
	fw4 reload >"/dev/null" 2>&1
 | 
			
		||||
 | 
			
		||||
	# Remove DNS hijack
 | 
			
		||||
	rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
 | 
			
		||||
	/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
 | 
			
		||||
 | 
			
		||||
	rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \
 | 
			
		||||
		"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log"
 | 
			
		||||
 | 
			
		||||
	log "Service stopped."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
service_stopped() {
 | 
			
		||||
	# Load config
 | 
			
		||||
	config_load "$CONF"
 | 
			
		||||
	local tun_name
 | 
			
		||||
	config_get tun_name "infra" "tun_name" "singtun0"
 | 
			
		||||
 | 
			
		||||
	# TUN
 | 
			
		||||
	ip link set "$tun_name" down 2>"/dev/null"
 | 
			
		||||
	ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
reload_service() {
 | 
			
		||||
	log "Reloading service..."
 | 
			
		||||
 | 
			
		||||
	stop
 | 
			
		||||
	start
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
service_triggers() {
 | 
			
		||||
	procd_add_reload_trigger "$CONF"
 | 
			
		||||
	procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload
 | 
			
		||||
}
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
uci -q batch <<-EOF >"/dev/null"
 | 
			
		||||
	delete firewall.homeproxy_pre
 | 
			
		||||
	set firewall.homeproxy_pre=include
 | 
			
		||||
	set firewall.homeproxy_pre.type=nftables
 | 
			
		||||
	set firewall.homeproxy_pre.path="/var/run/homeproxy/fw4_pre.nft"
 | 
			
		||||
	set firewall.homeproxy_pre.position="table-pre"
 | 
			
		||||
 | 
			
		||||
	delete firewall.homeproxy_post
 | 
			
		||||
	set firewall.homeproxy_post=include
 | 
			
		||||
	set firewall.homeproxy_post.type=nftables
 | 
			
		||||
	set firewall.homeproxy_post.path="/var/run/homeproxy/fw4_post.nft"
 | 
			
		||||
	set firewall.homeproxy_post.position="table-post"
 | 
			
		||||
	commit firewall
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
exit 0
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
china_dns_server="$(uci -q get "homeproxy.config.china_dns_server")"
 | 
			
		||||
if [ "$china_dns_server" = "wan_114" ]; then
 | 
			
		||||
	uci -q delete "homeproxy.config.china_dns_server"
 | 
			
		||||
	uci -q add_list "homeproxy.config.china_dns_server"="wan"
 | 
			
		||||
	uci -q add_list "homeproxy.config.china_dns_server"="114.114.114.114"
 | 
			
		||||
elif echo "$china_dns_server" | grep -q ","; then
 | 
			
		||||
	uci -q delete "homeproxy.config.china_dns_server"
 | 
			
		||||
	for dns in ${china_dns_server//,/ }; do
 | 
			
		||||
		uci -q add_list "homeproxy.config.china_dns_server"="$dns"
 | 
			
		||||
	done
 | 
			
		||||
fi
 | 
			
		||||
[ -z "$(uci -q changes "homeproxy")" ] || uci -q commit "homeproxy"
 | 
			
		||||
 | 
			
		||||
exit 0
 | 
			
		||||
@ -1,45 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"admin/services/homeproxy": {
 | 
			
		||||
		"title": "HomeProxy",
 | 
			
		||||
		"order": 10,
 | 
			
		||||
		"action": {
 | 
			
		||||
			"type": "firstchild"
 | 
			
		||||
		},
 | 
			
		||||
		"depends": {
 | 
			
		||||
			"acl": [ "luci-app-homeproxy" ],
 | 
			
		||||
			"uci": { "homeproxy": true }
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"admin/services/homeproxy/client": {
 | 
			
		||||
		"title": "Client Settings",
 | 
			
		||||
		"order": 10,
 | 
			
		||||
		"action": {
 | 
			
		||||
			"type": "view",
 | 
			
		||||
			"path": "homeproxy/client"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"admin/services/homeproxy/node": {
 | 
			
		||||
		"title": "Node Settings",
 | 
			
		||||
		"order": 15,
 | 
			
		||||
		"action": {
 | 
			
		||||
			"type": "view",
 | 
			
		||||
			"path": "homeproxy/node"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"admin/services/homeproxy/server": {
 | 
			
		||||
		"title": "Server Settings",
 | 
			
		||||
		"order": 20,
 | 
			
		||||
		"action": {
 | 
			
		||||
			"type": "view",
 | 
			
		||||
			"path": "homeproxy/server"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"admin/services/homeproxy/status": {
 | 
			
		||||
		"title": "Service Status",
 | 
			
		||||
		"order": 30,
 | 
			
		||||
		"action": {
 | 
			
		||||
			"type": "view",
 | 
			
		||||
			"path": "homeproxy/status"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"luci-app-homeproxy": {
 | 
			
		||||
		"description": "Grant access to homeproxy configuration",
 | 
			
		||||
		"read": {
 | 
			
		||||
			"file": {
 | 
			
		||||
				"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
 | 
			
		||||
				"/var/run/homeproxy/homeproxy.log": [ "read" ],
 | 
			
		||||
				"/var/run/homeproxy/sing-box-c.log": [ "read" ],
 | 
			
		||||
				"/var/run/homeproxy/sing-box-s.log": [ "read" ]
 | 
			
		||||
			},
 | 
			
		||||
			"ubus": {
 | 
			
		||||
				"service": [ "list" ],
 | 
			
		||||
				"luci.homeproxy": [ "*" ]
 | 
			
		||||
			},
 | 
			
		||||
			"uci": [ "homeproxy" ]
 | 
			
		||||
		},
 | 
			
		||||
		"write": {
 | 
			
		||||
			"file": {
 | 
			
		||||
				"/tmp/homeproxy_certificate.tmp": [ "write" ]
 | 
			
		||||
			},
 | 
			
		||||
			"uci": [ "homeproxy" ]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,207 +0,0 @@
 | 
			
		||||
#!/usr/bin/ucode
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-License-Identifier: GPL-2.0-only
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2023 ImmortalWrt.org
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
import { access, error, lstat, mkstemp, popen, readfile, writefile } from 'fs';
 | 
			
		||||
 | 
			
		||||
/* Kanged from ucode/luci */
 | 
			
		||||
function shellquote(s) {
 | 
			
		||||
	return `'${replace(s, "'", "'\\''")}'`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hasKernelModule(kmod) {
 | 
			
		||||
	return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HP_DIR = '/etc/homeproxy';
 | 
			
		||||
const RUN_DIR = '/var/run/homeproxy';
 | 
			
		||||
 | 
			
		||||
const methods = {
 | 
			
		||||
	acllist_read: {
 | 
			
		||||
		args: { type: 'type' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
 | 
			
		||||
				return { content: null, error: 'illegal type' };
 | 
			
		||||
 | 
			
		||||
			const filecontent = readfile(`${HP_DIR}/resources/${req.args?.type}.txt`);
 | 
			
		||||
			return { content: filecontent };
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	acllist_write: {
 | 
			
		||||
		args: { type: 'type', content: 'content' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
 | 
			
		||||
				return { result: false, error: 'illegal type' };
 | 
			
		||||
 | 
			
		||||
			const file = `${HP_DIR}/resources/${req.args?.type}.txt`;
 | 
			
		||||
			let content = req.args?.content;
 | 
			
		||||
 | 
			
		||||
			/* Sanitize content */
 | 
			
		||||
			if (content) {
 | 
			
		||||
				content = trim(content);
 | 
			
		||||
				content = replace(content, /\r\n?/g, '\n');
 | 
			
		||||
				if (!match(content, /\n$/))
 | 
			
		||||
					content += '\n';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			system(`mkdir -p ${HP_DIR}/resources`);
 | 
			
		||||
			writefile(file, content);
 | 
			
		||||
 | 
			
		||||
			return { result: true };
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	certificate_write: {
 | 
			
		||||
		args: { filename: 'filename' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			const writeCertificate = function(filename, priv) {
 | 
			
		||||
				const tmpcert = '/tmp/homeproxy_certificate.tmp';
 | 
			
		||||
				const filestat = lstat(tmpcert);
 | 
			
		||||
 | 
			
		||||
				if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
 | 
			
		||||
					system(`rm -f ${tmpcert}`);
 | 
			
		||||
					return { result: false, error: 'empty certificate file' };
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				let filecontent = readfile(tmpcert);
 | 
			
		||||
				if (is_binary(filecontent)) {
 | 
			
		||||
					system(`rm -f ${tmpcert}`);
 | 
			
		||||
					return { result: false, error: 'illegal file type: binary' };
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				/* Kanged from luci-proto-openconnect */
 | 
			
		||||
				const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
 | 
			
		||||
				      end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
 | 
			
		||||
				      lines = split(trim(filecontent), /[\r\n]/);
 | 
			
		||||
				let start = false, i;
 | 
			
		||||
 | 
			
		||||
				for (i = 0; i < length(lines); i++) {
 | 
			
		||||
					if (match(lines[i], beg))
 | 
			
		||||
						start = true;
 | 
			
		||||
					else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
 | 
			
		||||
					system(`rm -f ${tmpcert}`);
 | 
			
		||||
					return { result: false, error: 'this does not look like a correct PEM file' };
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				/* Sanitize certificate */
 | 
			
		||||
				filecontent = trim(filecontent);
 | 
			
		||||
				filecontent = replace(filecontent, /\r\n?/g, '\n');
 | 
			
		||||
				if (!match(filecontent, /\n$/))
 | 
			
		||||
					filecontent += '\n';
 | 
			
		||||
 | 
			
		||||
				system(`mkdir -p ${HP_DIR}/certs`);
 | 
			
		||||
				writefile(`${HP_DIR}/certs/${filename}.pem`, filecontent);
 | 
			
		||||
				system(`rm -f ${tmpcert}`);
 | 
			
		||||
 | 
			
		||||
				return { result: true };
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const filename = req.args?.filename;
 | 
			
		||||
			switch (filename) {
 | 
			
		||||
			case 'client_ca':
 | 
			
		||||
			case 'server_publickey':
 | 
			
		||||
				return writeCertificate(filename, false);
 | 
			
		||||
				break;
 | 
			
		||||
			case 'server_privatekey':
 | 
			
		||||
				return writeCertificate(filename, true);
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				return { result: false, error: 'illegal cerificate filename' };
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	connection_check: {
 | 
			
		||||
		args: { site: 'site' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			let url;
 | 
			
		||||
			switch(req.args?.site) {
 | 
			
		||||
			case 'baidu':
 | 
			
		||||
				url = 'https://www.baidu.com';
 | 
			
		||||
				break;
 | 
			
		||||
			case 'google':
 | 
			
		||||
				url = 'https://www.google.com';
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				return { result: false, error: 'illegal site' };
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return { result: (system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100) === 0) };
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	log_clean: {
 | 
			
		||||
		args: { type: 'type' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			if (!(req.args?.type in ['homeproxy', 'sing-box-c', 'sing-box-s']))
 | 
			
		||||
				return { result: false, error: 'illegal type' };
 | 
			
		||||
 | 
			
		||||
			const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`);
 | 
			
		||||
			if (filestat)
 | 
			
		||||
				writefile(`${RUN_DIR}/${req.args?.type}.log`, '');
 | 
			
		||||
			return { result: true };
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	singbox_get_features: {
 | 
			
		||||
		call: function() {
 | 
			
		||||
			let features = {};
 | 
			
		||||
 | 
			
		||||
			const fd = popen('/usr/bin/sing-box version');
 | 
			
		||||
			if (fd) {
 | 
			
		||||
				for (let line = fd.read('line'); length(line); line = fd.read('line')) {
 | 
			
		||||
					let tags = match(trim(line), /Tags: (.*)/);
 | 
			
		||||
					if (!tags)
 | 
			
		||||
						continue;
 | 
			
		||||
 | 
			
		||||
					for (let i in split(tags[1], ','))
 | 
			
		||||
						features[i] = true;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				fd.close();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			features.hp_has_chinadns_ng = access('/usr/bin/chinadns-ng');
 | 
			
		||||
			if (features.hp_has_chinadns_ng)
 | 
			
		||||
				features.hp_has_chinadns_ng_v2 = (system('/usr/bin/chinadns-ng --version | grep -q "target:"') === 0);
 | 
			
		||||
			features.hp_has_ip_full = access('/usr/libexec/ip-full');
 | 
			
		||||
			features.hp_has_tcp_brutal = hasKernelModule('brutal.ko');
 | 
			
		||||
			features.hp_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy');
 | 
			
		||||
			features.hp_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun');
 | 
			
		||||
 | 
			
		||||
			return features;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	resources_get_version: {
 | 
			
		||||
		args: { type: 'type' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			const version = trim(readfile(`${HP_DIR}/resources/${req.args?.type}.ver`));
 | 
			
		||||
			return { version: version, error: error() };
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	resources_update: {
 | 
			
		||||
		args: { type: 'type' },
 | 
			
		||||
		call: function(req) {
 | 
			
		||||
			if (req.args?.type) {
 | 
			
		||||
				const type = shellquote(req.args?.type);
 | 
			
		||||
				const exit_code = system(`${HP_DIR}/scripts/update_resources.sh ${type}`);
 | 
			
		||||
				return { status: exit_code };
 | 
			
		||||
			} else
 | 
			
		||||
				return { status: 255, error: 'illegal type' };
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
return { 'luci.homeproxy': methods };
 | 
			
		||||
@ -12,6 +12,8 @@ LUCI_DEPENDS:= \
 | 
			
		||||
	+firewall4 \
 | 
			
		||||
	+kmod-nft-tproxy
 | 
			
		||||
 | 
			
		||||
PKG_NAME:=luci-app-homeproxy
 | 
			
		||||
 | 
			
		||||
define Package/luci-app-homeproxy/conffiles
 | 
			
		||||
/etc/config/homeproxy
 | 
			
		||||
/etc/homeproxy/certs/
 | 
			
		||||
 | 
			
		||||
@ -912,7 +912,7 @@ return view.extend({
 | 
			
		||||
		so.modalonly = true;
 | 
			
		||||
 | 
			
		||||
		so = ss.taboption('field_other', form.MultiValue, 'outbound', _('Outbound'),
 | 
			
		||||
			_('Match outbound.'));
 | 
			
		||||
			_('Match <code>.outbounds[].server</code> domains.'));
 | 
			
		||||
		so.load = function(section_id) {
 | 
			
		||||
			delete this.keylist;
 | 
			
		||||
			delete this.vallist;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -63,3 +63,10 @@ config homeproxy 'server'
 | 
			
		||||
	option enabled '0'
 | 
			
		||||
	option auto_firewall '0'
 | 
			
		||||
 | 
			
		||||
config dns_rule 'nodes_domain'
 | 
			
		||||
	option label 'NodesDomain'
 | 
			
		||||
	option enabled '1'
 | 
			
		||||
	option mode 'default'
 | 
			
		||||
	list outbound 'any-out'
 | 
			
		||||
	option server 'default-dns'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user