update 2026-07-12 00:50:47

This commit is contained in:
action 2026-07-12 00:50:47 +08:00
parent 274b09a7d6
commit 638ee2bb02
20 changed files with 2117 additions and 2163 deletions

View File

@ -10,12 +10,18 @@ LUCI_DEPENDS:= \
+sing-box \
+firewall4 \
+kmod-nft-tproxy \
+curl \
+jsonfilter \
+ucode-mod-digest
PKG_NAME:=luci-app-homeproxy
PKG_VERSION=$(PKG_SRC_VERSION)
PKG_RELEASE:=5
PKG_PO_VERSION=$(PKG_SRC_VERSION)-r$(PKG_RELEASE)
define Package/luci-app-homeproxy/conffiles
/etc/config/homeproxy
/etc/homeproxy/cache/
/etc/homeproxy/certs/
/etc/homeproxy/ruleset/
/etc/homeproxy/resources/direct_list.txt

View File

@ -65,7 +65,7 @@ function renderStatus(isRunning, version, currentNode) {
renderHTML = spanTemp.format(statusColor, _('HomeProxy'), version, _('NOT RUNNING'));
if (currentNode)
renderHTML += '<div><em><span style="color:%s"><strong>%s</strong></span></em></div>'.format(nodeColor, currentNode);
renderHTML += '<div><em><span style="color:%s"><strong>%s</strong></span></em></div>'.format(nodeColor, '%h'.format(currentNode));
return renderHTML;
}
@ -101,12 +101,13 @@ return view.extend({
/* Cache all configured proxy nodes, they will be called multiple times */
let proxy_nodes = {};
uci.sections(data[0], 'node', (res) => {
let nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '',
nodeport = ((res.type === 'direct') ? res.override_port : res.port) || '';
let nodeaddr = res.address || '',
nodeport = res.port || '',
endpoint = nodeaddr && nodeport ? ((stubValidator.apply('ip6addr', nodeaddr) ?
String.format('[%s]', nodeaddr) : nodeaddr) + ':' + nodeport) : res['.name'];
proxy_nodes[res['.name']] =
String.format('[%s] %s', res.type, res.label || ((stubValidator.apply('ip6addr', nodeaddr) ?
String.format('[%s]', nodeaddr) : nodeaddr) + ':' + nodeport));
String.format('[%s] %s', res.type, res.label || endpoint);
});
m = new form.Map('homeproxy', _('HomeProxy'),
@ -348,13 +349,6 @@ return view.extend({
desc.innerHTML = _('Less compatibility and sometimes better performance.');
}
so = ss.option(form.Flag, 'endpoint_independent_nat', _('Enable endpoint-independent NAT'),
_('Performance may degrade slightly, so it is not recommended to enable on when it is not needed.'));
so.default = so.enabled;
so.depends('tcpip_stack', 'mixed');
so.depends('tcpip_stack', 'gvisor');
so.rmempty = false;
so = ss.option(form.Value, 'udp_timeout', _('UDP NAT expiration time'),
_('In seconds.'));
so.datatype = 'uinteger';
@ -1410,6 +1404,7 @@ return view.extend({
return this.super('load', section_id);
}
so.default = 'direct-out';
so.depends('type', 'remote');
so = ss.option(form.Value, 'update_interval', _('Update interval'),

View File

@ -923,15 +923,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode, no
}
o.modalonly = true;
/* Direct config */
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
_('Write proxy protocol in the connection header.'));
o.value('', _('Disable'));
o.value('1', _('v1'));
o.value('2', _('v2'));
o.depends('type', 'direct');
o.modalonly = true;
/* AnyTLS config start */
o = s.option(form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
_('Interval checking for idle sessions, in seconds.'));

View File

@ -60,56 +60,107 @@ function getConnStat(o, site) {
]);
}
function getResVersion(o, type) {
const callResVersion = rpc.declare({
const resources = [
{
type: 'china_ip4',
name: _('China IPv4 list')
},
{
type: 'china_ip6',
name: _('China IPv6 list')
},
{
type: 'china_list',
name: _('China domain list')
},
{
type: 'gfw_list',
name: _('GFW domain list')
}
];
function getResources(o) {
const callResStatus = rpc.declare({
object: 'luci.homeproxy',
method: 'resources_get_version',
params: ['type'],
method: 'resources_get',
expect: { '': {} }
});
const callResUpdate = rpc.declare({
object: 'luci.homeproxy',
method: 'resources_update',
params: ['type'],
expect: { '': {} }
});
return L.resolveDefault(callResVersion(type), {}).then((res) => {
let spanTemp = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, () => {
return L.resolveDefault(callResUpdate(type), {}).then((res) => {
switch (res.status) {
case 0:
o.description = _('Successfully updated.');
break;
case 1:
o.description = _('Update failed.');
break;
case 2:
o.description = _('Already in updating.');
break;
case 3:
o.description = _('Already at the latest version.');
break;
default:
o.description = _('Unknown error.');
break;
}
return o.map.reset();
});
})
}, [ _('Check update') ]),
' ',
E('strong', { 'style': (res.error ? 'color:red' : 'color:green') },
[ res.error ? 'not found' : res.version ]
),
return L.resolveDefault(callResStatus(), { resources: [] }).then((result) => {
const status = {};
(result.resources || []).forEach((resource) => {
status[resource.type] = resource;
});
const table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Name')),
E('th', { 'class': 'th' }, _('Version')),
E('th', { 'class': 'th' }, _('Source'))
])
]);
const rows = resources.map((resource) => {
const resourceStatus = status[resource.type] || {};
const available = resourceStatus.version;
const source = resourceStatus.source;
o.default = spanTemp;
return [
resource.name,
E('span', { 'style': available ? 'color:green' : 'color:red' },
available || '-'),
source ? E('a', {
'href': source,
'target': '_blank',
'rel': 'noreferrer noopener',
'style': 'word-break:break-all'
}, source) : '-'
];
});
cbi_update_table(table, rows);
return E('div', { 'class': 'cbi-map' }, [
E('h3', { 'name': 'content', 'style': 'align-items:center;display:flex' }, [
_('Resources management'),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'style': 'margin-left:4px',
'click': ui.createHandlerFn(this, () => {
return L.resolveDefault(callResUpdate(), {}).then((res) => {
let message, severity = 'info';
switch (res.status) {
case 0:
message = _('Successfully updated.');
break;
case 1:
message = _('Update failed.');
severity = 'error';
break;
case 2:
message = _('Already in updating.');
break;
case 3:
message = _('Already at the latest version.');
break;
default:
message = _('Unknown error.');
severity = 'error';
break;
}
ui.addNotification(null, E('p', message), severity);
return o.map.reset();
});
})
}, [ _('Update all') ])
]),
E('div', { 'class': 'cbi-section' }, [ table ])
]);
});
}
@ -238,24 +289,11 @@ return view.extend({
o = s.option(form.DummyValue, '_check_google', _('Google'));
o.cfgvalue = L.bind(getConnStat, this, o, 'google');
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
s = m.section(form.NamedSection, 'config', 'homeproxy');
s.anonymous = true;
o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
o.cfgvalue = L.bind(getResVersion, this, o, 'china_ip4');
o.rawhtml = true;
o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
o.cfgvalue = L.bind(getResVersion, this, o, 'china_ip6');
o.rawhtml = true;
o = s.option(form.DummyValue, '_china_list_version', _('China list version'));
o.cfgvalue = L.bind(getResVersion, this, o, 'china_list');
o.rawhtml = true;
o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list');
o.rawhtml = true;
o = s.option(form.DummyValue, '_resources');
o.render = L.bind(getResources, this, o);
o = s.option(form.Value, 'github_token', _('GitHub token'));
o.password = true;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ import { cursor } from 'uci';
import {
isEmpty, parseURL, strToBool, strToInt, strToTime,
removeBlankAttrs, validation, HP_DIR, RUN_DIR
removeBlankAttrs, renderEndpoint, renderOutbound, validation, HP_DIR, RUN_DIR
} from 'homeproxy';
const ubus = connect();
@ -117,8 +117,7 @@ const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy
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, tcpip_stack,
endpoint_independent_nat, udp_timeout;
tun_addr4, tun_addr6, tun_mtu, tcpip_stack, udp_timeout;
if (routing_mode === 'custom')
udp_timeout = uci.get(uciconfig, uciroutingsetting, 'udp_timeout');
@ -140,7 +139,6 @@ if (match(proxy_mode, /tun/)) {
tcpip_stack = 'system';
if (routing_mode === 'custom') {
tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system';
endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
}
}
@ -198,168 +196,10 @@ function filter_existing_nodes(nodes) {
});
}
function generate_endpoint(node) {
if (type(node) !== 'object' || isEmpty(node))
return null;
const endpoint = {
type: node.type,
tag: 'cfg-' + node['.name'] + '-out',
address: node.wireguard_local_address,
mtu: strToInt(node.wireguard_mtu),
private_key: node.wireguard_private_key,
peers: (node.type === 'wireguard') ? [
{
address: node.address,
port: strToInt(node.port),
allowed_ips: [
'0.0.0.0/0',
'::/0'
],
persistent_keepalive_interval: strToInt(node.wireguard_persistent_keepalive_interval),
public_key: node.wireguard_peer_public_key,
pre_shared_key: node.wireguard_pre_shared_key,
reserved: parse_port(node.wireguard_reserved),
}
] : null,
system: (node.type === 'wireguard') ? false : null,
tcp_fast_open: strToBool(node.tcp_fast_open),
tcp_multi_path: strToBool(node.tcp_multi_path),
udp_fragment: strToBool(node.udp_fragment)
};
return endpoint;
}
const generate_endpoint = renderEndpoint;
function generate_outbound(node) {
if (type(node) !== 'object' || isEmpty(node))
return null;
const tls_utls_value = (node.type === 'anytls' && isEmpty(node.tls_utls)) ? 'chrome' : node.tls_utls;
const outbound = {
type: node.type,
tag: 'cfg-' + node['.name'] + '-out',
routing_mark: strToInt(self_mark),
server: node.address,
server_port: strToInt(node.port),
/* Hysteria(2) */
server_ports: node.hysteria_hopping_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),
proxy_protocol: strToInt(node.proxy_protocol),
/* AnyTLS */
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
min_idle_session: strToInt(node.anytls_min_idle_session),
/* Hysteria (2) */
hop_interval: strToTime(node.hysteria_hop_interval),
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: strToTime(node.tuic_heartbeat),
/* VLESS / VMess */
flow: node.vless_flow,
alter_id: strToInt(node.vmess_alterid),
security: node.vmess_encrypt,
global_padding: strToBool(node.vmess_global_padding),
authenticated_length: strToBool(node.vmess_authenticated_length),
packet_encoding: node.packet_encoding,
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: strToBool(node.multiplex_padding),
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: strToBool(node.tls_insecure),
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,
config: node.tls_ech_config,
config_path: node.tls_ech_config_path
} : null,
utls: !isEmpty(tls_utls_value) ? {
enabled: true,
fingerprint: tls_utls_value
} : 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: strToTime(node.http_idle_timeout),
ping_timeout: strToTime(node.http_ping_timeout),
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;
return renderOutbound(node, self_mark);
}
function get_outbound(cfg) {
@ -586,7 +426,6 @@ if (!isEmpty(main_node)) {
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
rule_set_ip_cidr_accept_empty: strToBool(cfg.rule_set_ip_cidr_accept_empty),
invert: strToBool(cfg.invert),
outbound: get_outbound(cfg.outbound),
action: cfg.action,
server: get_resolver(cfg.server),
strategy: cfg.domain_strategy,
@ -655,7 +494,6 @@ if (match(proxy_mode, /tun/))
address: (ipv6_support === '1') ? [tun_addr4, tun_addr6] : [tun_addr4],
mtu: strToInt(tun_mtu),
auto_route: false,
endpoint_independent_nat: strToBool(endpoint_independent_nat),
udp_timeout: strToTime(udp_timeout),
stack: tcpip_stack
});
@ -865,7 +703,6 @@ config.route = {
if (!isEmpty(main_node)) {
/* Avoid DNS loop */
config.route.default_domain_resolver = {
action: 'route',
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns',
strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null
};
@ -919,21 +756,21 @@ if (!isEmpty(main_node)) {
tag: 'geoip-cn',
format: 'binary',
url: 'https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs',
download_detour: 'main-out'
download_detour: 'direct-out'
});
push(config.route.rule_set, {
type: 'remote',
tag: 'geosite-cn',
format: 'binary',
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs',
download_detour: 'main-out'
download_detour: 'direct-out'
});
push(config.route.rule_set, {
type: 'remote',
tag: 'geosite-noncn',
format: 'binary',
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs',
download_detour: 'main-out'
download_detour: 'direct-out'
});
}
@ -941,7 +778,6 @@ if (!isEmpty(main_node)) {
config.route.rule_set = null;
} else if (!isEmpty(default_outbound)) {
config.route.default_domain_resolver = {
action: 'resolve',
server: get_resolver(default_outbound_dns)
};
@ -1004,7 +840,7 @@ if (!isEmpty(main_node)) {
format: cfg.format,
path: cfg.path,
url: cfg.url,
download_detour: get_outbound(cfg.outbound),
download_detour: get_outbound(cfg.outbound) || ((cfg.type === 'remote') ? 'direct-out' : null),
update_interval: cfg.update_interval
});
});
@ -1019,7 +855,7 @@ if (routing_mode in ['bypass_mainland_china', 'custom']) {
},
cache_file: {
enabled: true,
path: RUN_DIR + '/cache.db',
path: HP_DIR + '/cache/cache.db',
store_rdrc: strToBool(cache_file_store_rdrc),
rdrc_timeout: strToTime(cache_file_rdrc_timeout),
}
@ -1028,4 +864,5 @@ if (routing_mode in ['bypass_mainland_china', 'custom']) {
/* Experimental end */
system('mkdir -p ' + RUN_DIR);
writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config)));
if (!writefile(RUN_DIR + '/sing-box-c.json.new', sprintf('%.J\n', removeBlankAttrs(config))))
exit(1);

View File

@ -129,7 +129,7 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
email: cfg.tls_acme_email,
provider: cfg.tls_acme_provider,
disable_http_challenge: strToBool(cfg.tls_acme_dhc),
disable_tls_alpn_challenge: (cfg.tls_acme_dtac),
disable_tls_alpn_challenge: strToBool(cfg.tls_acme_dtac),
alternative_http_port: strToInt(cfg.tls_acme_ahp),
alternative_tls_port: strToInt(cfg.tls_acme_atp),
external_account: (cfg.tls_acme_external_account === '1') ? {
@ -182,4 +182,5 @@ if (length(config.inbounds) === 0)
exit(1);
system('mkdir -p ' + RUN_DIR);
writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config)));
if (!writefile(RUN_DIR + '/sing-box-s.json.new', sprintf('%.J\n', removeBlankAttrs(config))))
exit(1);

View File

@ -95,6 +95,161 @@ export function strToTime(str) {
return !isEmpty(str) ? (str + 's') : null;
};
function strListToInts(value) {
if (type(value) !== 'array' || isEmpty(value))
return null;
return map(value, (item) => int(item));
};
export function renderEndpoint(node) {
if (type(node) !== 'object' || isEmpty(node))
return null;
return {
type: node.type,
tag: 'cfg-' + node['.name'] + '-out',
address: node.wireguard_local_address,
mtu: strToInt(node.wireguard_mtu),
private_key: node.wireguard_private_key,
peers: (node.type === 'wireguard') ? [
{
address: node.address,
port: strToInt(node.port),
allowed_ips: [
'0.0.0.0/0',
'::/0'
],
persistent_keepalive_interval: strToInt(node.wireguard_persistent_keepalive_interval),
public_key: node.wireguard_peer_public_key,
pre_shared_key: node.wireguard_pre_shared_key,
reserved: strListToInts(node.wireguard_reserved)
}
] : null,
system: (node.type === 'wireguard') ? false : null,
tcp_fast_open: strToBool(node.tcp_fast_open),
tcp_multi_path: strToBool(node.tcp_multi_path),
udp_fragment: strToBool(node.udp_fragment)
};
};
export function renderOutbound(node, routingMark) {
if (type(node) !== 'object' || isEmpty(node))
return null;
const tls_utls_value = (node.type === 'anytls' && isEmpty(node.tls_utls)) ? 'chrome' : node.tls_utls;
return {
type: node.type,
tag: 'cfg-' + node['.name'] + '-out',
routing_mark: strToInt(routingMark),
server: node.address,
server_port: strToInt(node.port),
server_ports: node.hysteria_hopping_port,
username: (node.type !== 'ssh') ? node.username : null,
user: (node.type === 'ssh') ? node.username : null,
password: node.password,
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
min_idle_session: strToInt(node.anytls_min_idle_session),
hop_interval: strToTime(node.hysteria_hop_interval),
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),
method: node.shadowsocks_encrypt_method,
plugin: node.shadowsocks_plugin,
plugin_opts: node.shadowsocks_plugin_opts,
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null),
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,
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: strToTime(node.tuic_heartbeat),
flow: node.vless_flow,
alter_id: strToInt(node.vmess_alterid),
security: node.vmess_encrypt,
global_padding: strToBool(node.vmess_global_padding),
authenticated_length: strToBool(node.vmess_authenticated_length),
packet_encoding: node.packet_encoding,
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: strToBool(node.multiplex_padding),
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: strToBool(node.tls_insecure),
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,
config: node.tls_ech_config,
config_path: node.tls_ech_config_path
} : null,
utls: !isEmpty(tls_utls_value) ? {
enabled: true,
fingerprint: tls_utls_value
} : 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: strToTime(node.http_idle_timeout),
ping_timeout: strToTime(node.http_ping_timeout),
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)
};
};
export function removeBlankAttrs(res) {
let content;

View File

@ -55,6 +55,10 @@ if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
uci.delete(uciconfig, uciinfra, 'tun_gso');
/* endpoint_independent_nat has been removed */
if (!isEmpty(uci.get(uciconfig, ucirouting, 'endpoint_independent_nat')))
uci.delete(uciconfig, ucirouting, 'endpoint_independent_nat');
/* create migration section */
if (!uci.get(uciconfig, ucimigration))
uci.set(uciconfig, ucimigration, uciconfig);
@ -99,6 +103,9 @@ uci.foreach(uciconfig, ucidnsserver, (cfg) => {
/* legacy format was deprecated in sb 1.12 */
if (cfg.address) {
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
if (!addr)
return;
/* RCode was moved into DNS rules */
if (addr.protocol === 'rcode') {
dns_server_migration[cfg['.name']] = { action: 'predefined' };
@ -157,10 +164,11 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
/* outbound was removed in sb 1.12 */
if (cfg.outbound) {
uci.delete(uciconfig, cfg['.name']);
if (!cfg.enabled)
if (cfg.enabled !== '1')
return;
map(cfg.outbound, (outbound) => {
const outbounds = (type(cfg.outbound) === 'array') ? cfg.outbound : [ cfg.outbound ];
for (let outbound in outbounds) {
switch (outbound) {
case 'direct-out':
case 'block-out':
@ -169,16 +177,16 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
uci.set(uciconfig, ucirouting, 'default_outbound_dns', cfg.server);
break;
default:
uci.set(uciconfig, cfg.outbound, 'domain_resolver', cfg.server);
break;
uci.set(uciconfig, outbound, 'domain_resolver', cfg.server);
break;
}
});
}
return;
}
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
if (!isEmpty(cfg.rule_set_ipcidr_match_source))
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
/* block-dns was moved into action in sb 1.11 */
@ -208,6 +216,11 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
/* nodes options */
uci.foreach(uciconfig, ucinode, (cfg) => {
/* destination override and Proxy Protocol were removed from direct outbound */
for (let option in ['override_address', 'override_port', 'proxy_protocol'])
if (!isEmpty(cfg[option]))
uci.delete(uciconfig, cfg['.name'], option);
/* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
@ -224,7 +237,7 @@ uci.foreach(uciconfig, ucinode, (cfg) => {
/* routing rules options */
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
if (!isEmpty(cfg.rule_set_ipcidr_match_source))
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
/* block-out was moved into action in sb 1.11 */

View File

@ -5,8 +5,6 @@
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_resources.sh
"$SCRIPTS_DIR"/update_subscriptions.uc

View File

@ -5,85 +5,102 @@
NAME="homeproxy"
RESOURCES_DIR="/etc/$NAME/resources"
mkdir -p "$RESOURCES_DIR"
RUN_DIR="/var/run/$NAME"
RESOURCES_DIR="${RESOURCES_DIR:-/etc/$NAME/resources}"
RUN_DIR="${RUN_DIR:-/var/run/$NAME}"
LOG_PATH="$RUN_DIR/$NAME.log"
mkdir -p "$RUN_DIR"
REPO_NAME="Loyalsoldier/surge-rules"
REPO_BRANCH="release"
SOURCE_BASE="${SOURCE_BASE:-https://cdn.jsdelivr.net/gh/$REPO_NAME@$REPO_BRANCH}"
RELEASE_API="${RELEASE_API:-https://api.github.com/repos/$REPO_NAME/releases/latest}"
mkdir -p "$RESOURCES_DIR" "$RUN_DIR"
log() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH"
printf '%s %s\n' "$(date "+%Y-%m-%d %H:%M:%S")" "$*" >> "$LOG_PATH"
}
to_upper() {
echo -e "$1" | tr "[a-z]" "[A-Z]"
printf '%s\n' "$1" | tr '[:lower:]' '[:upper:]'
}
check_list_update() {
local LIST_FILE="$1"
local REPO_NAME="$2"
local REPO_BRANCH="$3"
local REPO_FILE="$4"
local LOCK_FILE="$RUN_DIR/update_resources-$LIST_FILE.lock"
local GITHUB_TOKEN="$(uci -q get homeproxy.config.github_token)"
download() {
local source_file="$1"
local target_file="$2"
exec 200>"$LOCK_FILE"
if ! flock -n 200 &> "/dev/null"; then
log "[$(to_upper "$LIST_FILE")] A task is already running."
return 2
fi
local NEW_VER=$(curl -sL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} "https://api.github.com/repos/$REPO_NAME/releases/latest" | jsonfilter -e "@.tag_name")
if [ -z "$NEW_VER" ]; then
log "[$(to_upper "$LIST_FILE")] Failed to get the latest version, please retry later."
return 1
fi
local OLD_VER=$(cat "$RESOURCES_DIR/$LIST_FILE.ver" 2>/dev/null || echo "NOT FOUND")
if [ "$OLD_VER" = "$NEW_VER" ]; then
log "[$(to_upper "$LIST_FILE")] Current version: $NEW_VER."
log "[$(to_upper "$LIST_FILE")] You're already at the latest version."
return 3
else
log "[$(to_upper "$LIST_FILE")] Local version: $OLD_VER, latest version: $NEW_VER."
fi
if ! curl -sL -o "$RUN_DIR/$REPO_FILE" "https://cdn.jsdelivr.net/gh/$REPO_NAME@$REPO_BRANCH/$REPO_FILE" || [ ! -s "$RUN_DIR/$REPO_FILE" ]; then
rm -f "$RUN_DIR/$REPO_FILE"
log "[$(to_upper "$LIST_FILE")] Update failed."
return 1
fi
mv -f "$RUN_DIR/$REPO_FILE" "$RESOURCES_DIR/$LIST_FILE.${REPO_FILE##*.}"
echo -e "$NEW_VER" > "$RESOURCES_DIR/$LIST_FILE.ver"
log "[$(to_upper "$LIST_FILE")] Successfully updated."
return 0
curl -fsSL --retry 2 --connect-timeout 10 --max-time 60 \
-o "$target_file" "$SOURCE_BASE/$source_file" && [ -s "$target_file" ]
}
case "$1" in
"china_ip4")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
sed -i "/IP-CIDR6,/d; s/IP-CIDR,//g" "$RESOURCES_DIR/china_ip4.txt"
;;
"china_ip6")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
sed -i "/IP-CIDR,/d; s/IP-CIDR6,//g" "$RESOURCES_DIR/china_ip6.txt"
;;
"gfw_list")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "gfw.txt" && \
sed -i "s/^\.//g" "$RESOURCES_DIR/gfw_list.txt"
;;
"china_list")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "direct.txt" && \
sed -i "s/^\.//g" "$RESOURCES_DIR/china_list.txt"
;;
*)
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
exec 9>"$RUN_DIR/update_resources.lock"
if ! flock -n 9 > "/dev/null" 2>&1; then
log "[RESOURCES] A task is already running."
exit 2
fi
GITHUB_TOKEN="${GITHUB_TOKEN:-$(uci -q get homeproxy.config.github_token)}"
if [ -n "$GITHUB_TOKEN" ]; then
NEW_VER="$(curl -fsSL --retry 2 --connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer $GITHUB_TOKEN" \
"$RELEASE_API" | jsonfilter -e "@.tag_name")"
else
NEW_VER="$(curl -fsSL --retry 2 --connect-timeout 10 --max-time 30 \
"$RELEASE_API" | jsonfilter -e "@.tag_name")"
fi
if [ -z "$NEW_VER" ]; then
log "[RESOURCES] Failed to get the latest version, please retry later."
exit 1
;;
esac
fi
CURRENT=1
for RESOURCE in china_ip4 china_ip6 china_list gfw_list; do
OLD_VER="$(cat "$RESOURCES_DIR/$RESOURCE.ver" 2>/dev/null || echo "NOT FOUND")"
if [ "$OLD_VER" = "$NEW_VER" ]; then
log "[$(to_upper "$RESOURCE")] Current version: $NEW_VER."
else
CURRENT=0
log "[$(to_upper "$RESOURCE")] Local version: $OLD_VER, latest version: $NEW_VER."
fi
done
[ "$CURRENT" -eq 0 ] || {
log "[RESOURCES] You're already at the latest version."
exit 3
}
TMP_DIR="$(mktemp -d "$RUN_DIR/resources-update.XXXXXX")" || exit 1
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
if ! download "cncidr.txt" "$TMP_DIR/cncidr.txt" || \
! download "direct.txt" "$TMP_DIR/china_list.txt" || \
! download "gfw.txt" "$TMP_DIR/gfw_list.txt"; then
log "[RESOURCES] Update failed while downloading source lists."
exit 1
fi
cp "$TMP_DIR/cncidr.txt" "$TMP_DIR/china_ip4.txt" && \
cp "$TMP_DIR/cncidr.txt" "$TMP_DIR/china_ip6.txt" && \
sed -i "/IP-CIDR6,/d; s/IP-CIDR,//g" "$TMP_DIR/china_ip4.txt" && \
sed -i "/IP-CIDR,/d; s/IP-CIDR6,//g" "$TMP_DIR/china_ip6.txt" && \
sed -i "s/^\\.//g" "$TMP_DIR/china_list.txt" "$TMP_DIR/gfw_list.txt"
if [ "$?" -ne 0 ]; then
log "[RESOURCES] Update failed while processing source lists."
exit 1
fi
for RESOURCE in china_ip4 china_ip6 china_list gfw_list; do
if [ ! -s "$TMP_DIR/$RESOURCE.txt" ] || grep -qi '<html' "$TMP_DIR/$RESOURCE.txt"; then
log "[$(to_upper "$RESOURCE")] Update failed: invalid processed list."
exit 1
fi
printf '%s\n' "$NEW_VER" > "$TMP_DIR/$RESOURCE.ver" || exit 1
done
for RESOURCE in china_ip4 china_ip6 china_list gfw_list; do
if ! mv -f "$TMP_DIR/$RESOURCE.txt" "$RESOURCES_DIR/$RESOURCE.txt" || \
! mv -f "$TMP_DIR/$RESOURCE.ver" "$RESOURCES_DIR/$RESOURCE.ver"; then
log "[$(to_upper "$RESOURCE")] Update failed: unable to replace list."
exit 1
fi
log "[$(to_upper "$RESOURCE")] Successfully updated."
done
exit 0

View File

@ -17,7 +17,7 @@ import { init_action } from 'luci.sys';
import {
wGET, decodeBase64Str, getTime, isEmpty, parseURL,
validation, HP_DIR, RUN_DIR
shellQuote, validation, HP_DIR, RUN_DIR
} from 'homeproxy';
/* UCI config start */
@ -80,6 +80,18 @@ function log(...args) {
logfile.close();
}
if (getenv('HOMEPROXY_SUBSCRIPTION_LOCKED') !== '1') {
const lock_path = `${RUN_DIR}/update_subscriptions.lock`;
const script_path = `${HP_DIR}/scripts/update_subscriptions.uc`;
const status = system(sprintf(
'HOMEPROXY_SUBSCRIPTION_LOCKED=1 flock -n %s %s',
shellQuote(lock_path), shellQuote(script_path)
));
if (status !== 0)
log('Subscription update did not complete; another task may be running.');
exit(status);
}
function has_value(value) {
return value !== null && value !== '' && value !== 'nil';
}
@ -1000,7 +1012,8 @@ function main() {
const label = config.label;
config.label = null;
const confHash = md5(sprintf('%J', config)),
nameHash = md5(label);
nameHash = md5(groupHash + ':' + label),
legacyNameHash = md5(label);
config.label = label;
if (filter_check(config.label))
@ -1018,6 +1031,7 @@ function main() {
push(node_result[length(node_result)-1], config);
node_cache[groupHash][confHash] = config;
node_cache[groupHash][nameHash] = config;
node_cache[groupHash][legacyNameHash] = config;
count++;
}
@ -1040,7 +1054,7 @@ function main() {
return false;
}
let added = 0, removed = 0;
let added = 0, removed = 0, updated = 0;
uci.foreach(uciconfig, ucinode, (cfg) => {
/* Nodes created by the user */
if (!cfg.grouphash)
@ -1054,50 +1068,54 @@ function main() {
uci.delete(uciconfig, cfg['.name']);
removed++;
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
log(sprintf('Removing node: %s.', cfg.label || cfg['.name']));
} else {
map(keys(cfg), (v) => {
if (v in node_cache[cfg.grouphash][cfg['.name']])
uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
else
uci.delete(uciconfig, cfg['.name'], v);
});
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
const next = node_cache[cfg.grouphash][cfg['.name']];
for (let option in keys(cfg))
if (!match(option, /^\./) && !(option in next))
uci.delete(uciconfig, cfg['.name'], option);
for (let option in keys(next))
if (option !== 'isExisting')
uci.set(uciconfig, cfg['.name'], option, next[option]);
next.isExisting = true;
updated++;
}
});
for (let nodes in node_result)
map(nodes, (node) => {
if (node.isExisting)
return null;
const nameHash = md5(node.label);
const nameHash = md5(node.grouphash + ':' + 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) {
let main_urltest_nodes;
if (main_node === 'urltest') {
main_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_urltest_nodes'), (v) => {
main_urltest_nodes = filter(normalize_list(uci.get(uciconfig, ucimain, 'main_urltest_nodes')) || [], (v) => {
if (!uci.get(uciconfig, v)) {
log(sprintf('Node %s is gone, removing from urltest list.', v));
return false;
}
return true;
});
if (length(main_urltest_nodes))
uci.set(uciconfig, ucimain, 'main_urltest_nodes', main_urltest_nodes);
}
if ((main_node === 'urltest') ? !length(main_urltest_nodes) : !uci.get(uciconfig, main_node)) {
uci.set(uciconfig, ucimain, 'main_node', first_server);
uci.commit(uciconfig);
need_restart = true;
uci.delete(uciconfig, ucimain, 'main_urltest_nodes');
log('Main node is gone, switching to the first node.');
}
@ -1105,19 +1123,20 @@ function main() {
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
let main_udp_urltest_nodes;
if (main_udp_node === 'urltest') {
main_udp_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes'), (v) => {
main_udp_urltest_nodes = filter(normalize_list(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes')) || [], (v) => {
if (!uci.get(uciconfig, v)) {
log(sprintf('Node %s is gone, removing from urltest list.', v));
return false;
}
return true;
});
if (length(main_udp_urltest_nodes))
uci.set(uciconfig, ucimain, 'main_udp_urltest_nodes', main_udp_urltest_nodes);
}
if ((main_udp_node === 'urltest') ? !length(main_udp_urltest_nodes) : !uci.get(uciconfig, main_udp_node)) {
uci.set(uciconfig, ucimain, 'main_udp_node', first_server);
uci.commit(uciconfig);
need_restart = true;
uci.delete(uciconfig, ucimain, 'main_udp_urltest_nodes');
log('Main UDP node is gone, switching to the first node.');
}
@ -1125,20 +1144,19 @@ function main() {
} else {
uci.set(uciconfig, ucimain, 'main_node', 'nil');
uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
uci.commit(uciconfig);
need_restart = true;
uci.delete(uciconfig, ucimain, 'main_urltest_nodes');
uci.delete(uciconfig, ucimain, 'main_udp_urltest_nodes');
log('No available node, disable tproxy.');
}
}
uci.commit(uciconfig);
if (need_restart) {
log('Restarting service...');
init_action('homeproxy', 'stop');
init_action('homeproxy', 'start');
}
log('Restarting service...');
init_action('homeproxy', 'stop');
init_action('homeproxy', 'start');
log(sprintf('%s nodes added, %s removed.', added, removed));
log(sprintf('%s nodes added, %s updated, %s removed.', added, updated, removed));
log('Successfully updated subscriptions.');
}

View File

@ -14,6 +14,8 @@ PROG="/usr/bin/sing-box"
HP_DIR="/etc/homeproxy"
RUN_DIR="/var/run/homeproxy"
LOG_PATH="$RUN_DIR/homeproxy.log"
CACHE_DIR="$HP_DIR/cache"
CACHE_PATH="$CACHE_DIR/cache.db"
# we don't know which is the default server, just take the first one
DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
@ -27,6 +29,23 @@ log() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
}
validate_cron() {
local expression="$1" field
case "$expression" in
""|*[!0-9*/?,\ -]*) return 1 ;;
esac
set -f
set -- $expression
set +f
[ "$#" -eq 5 ] || return 1
for field in "$@"; do
[ -n "$field" ] || return 1
done
}
start_service() {
config_load "$CONF"
@ -49,26 +68,65 @@ start_service() {
fi
mkdir -p "$RUN_DIR"
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
# Generate and validate every enabled instance before changing system state.
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
if ! ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH" || \
[ ! -s "$RUN_DIR/sing-box-c.json.new" ]; then
log "Error: failed to generate client configuration."
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
return 1
elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then
elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json.new" 2>>"$LOG_PATH"; then
log "Error: wrong client configuration detected."
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
return 1
fi
fi
if [ "$server_enabled" = "1" ]; then
if ! ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH" || \
[ ! -s "$RUN_DIR/sing-box-s.json.new" ]; then
log "Error: failed to generate server configuration."
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
return 1
elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json.new" 2>>"$LOG_PATH"; then
log "Error: wrong server configuration detected."
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
return 1
fi
fi
if { [ "$outbound_node" = "nil" ] || mv -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-c.json"; } && \
{ [ "$server_enabled" = "0" ] || mv -f "$RUN_DIR/sing-box-s.json.new" "$RUN_DIR/sing-box-s.json"; }; then
[ "$outbound_node" != "nil" ] || rm -f "$RUN_DIR/sing-box-c.json"
[ "$server_enabled" = "1" ] || rm -f "$RUN_DIR/sing-box-s.json"
else
log "Error: failed to install generated configuration."
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
return 1
fi
case "$routing_mode" in
"bypass_mainland_china"|"custom")
mkdir -p "$CACHE_DIR"
touch "$CACHE_PATH"
chown -R sing-box:sing-box "$CACHE_DIR"
;;
esac
if [ "$outbound_node" != "nil" ]; then
# 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" "0 */6 * * *"
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
echo -e "$auto_update_time $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
if validate_cron "$auto_update_time"; then
printf '%s %s #%s_autosetup\n' "$auto_update_time" "$HP_DIR/scripts/update_crond.sh" "$CONF" >> "/etc/crontabs/root"
else
log "Error: invalid subscription update schedule."
fi
/etc/init.d/cron restart
fi
@ -79,7 +137,29 @@ start_service() {
mkdir -p "$DNSMASQ_DIR"
echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
case "$routing_mode" in
"bypass_mainland_china"|"custom"|"global")
"bypass_mainland_china")
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
no-poll
no-resolv
server=127.0.0.1#$dns_port
EOF
local wan_interface wan_dns china_dns candidate
. /lib/functions/network.sh
if network_find_wan wan_interface && network_get_dnsserver wan_dns "$wan_interface"; then
for candidate in $wan_dns; do
case "$candidate" in
*:*) ;;
*) china_dns="$candidate"; break ;;
esac
done
[ -n "$china_dns" ] || china_dns="${wan_dns%% *}"
fi
china_dns="${china_dns:-223.5.5.5}"
sed -r -e '/^\s*$/d' -e "s|(.*)|server=/\1/$china_dns|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
@ -156,7 +236,7 @@ start_service() {
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"
[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$RUN_DIR/cache.db"
[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$CACHE_PATH"
procd_add_jail_mount "$HP_DIR/certs/"
procd_add_jail_mount "/etc/ssl/"
procd_add_jail_mount "/etc/localtime"
@ -176,17 +256,6 @@ start_service() {
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"
@ -226,10 +295,6 @@ start_service() {
procd_close_instance
case "$routing_mode" in
"bypass_mainland_china")
# Prepare cache db
[ -e "$RUN_DIR/cache.db" ] || touch "$RUN_DIR/cache.db"
;;
"custom")
# Prepare ruleset directory
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
@ -308,8 +373,8 @@ stop_service() {
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"
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-c.log" \
"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.json.new" "$RUN_DIR/sing-box-s.log"
log "Service stopped."
}

View File

@ -1,5 +1,3 @@
#!/bin/sh
ucode "/etc/homeproxy/scripts/migrate_config.uc"
exit 0
exec ucode "/etc/homeproxy/scripts/migrate_config.uc"

View File

@ -3,21 +3,30 @@
"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": [ "*" ]
"luci.homeproxy": [
"acllist_read", "connection_check", "current_node_get",
"node_latency_test", "resources_get",
"singbox_generator", "singbox_get_features"
]
},
"uci": [ "homeproxy" ]
},
"write": {
"file": {
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
"/tmp/homeproxy_certificate.tmp": [ "write" ]
},
"ubus": {
"luci.homeproxy": [
"acllist_write", "certificate_write", "log_clean", "resources_update"
]
},
"uci": [ "homeproxy" ]
}
}

View File

@ -7,9 +7,13 @@
'use strict';
import { access, error, lstat, popen, readfile, writefile } from 'fs';
import { access, lstat, popen, readfile, writefile } from 'fs';
import { cursor } from 'uci';
import {
removeBlankAttrs, renderEndpoint, renderOutbound
} from '/etc/homeproxy/scripts/homeproxy.uc';
/* Kanged from ucode/luci */
function shellquote(s) {
return `'${replace(s, "'", "'\\''")}'`;
@ -21,6 +25,12 @@ function hasKernelModule(kmod) {
const HP_DIR = '/etc/homeproxy';
const RUN_DIR = '/var/run/homeproxy';
const RESOURCE_SOURCES = {
china_ip4: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/cncidr.txt',
china_ip6: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/cncidr.txt',
china_list: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/direct.txt',
gfw_list: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/gfw.txt'
};
const methods = {
acllist_read: {
@ -60,7 +70,7 @@ const methods = {
certificate_write: {
args: { filename: 'filename' },
call: function(req) {
const writeCertificate = (filename, priv) => {
const writeCertificate = (filename, kind) => {
const tmpcert = '/tmp/homeproxy_certificate.tmp';
const filestat = lstat(tmpcert);
@ -76,8 +86,10 @@ const methods = {
}
/* Kanged from luci-proto-openconnect */
const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
const beg = (kind === 'private') ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ :
((kind === 'ech') ? /^-----BEGIN ECH CONFIGS-----$/ : /^-----BEGIN CERTIFICATE-----$/),
end = (kind === 'private') ? /^-----END (RSA|EC) PRIVATE KEY-----$/ :
((kind === 'ech') ? /^-----END ECH CONFIGS-----$/ : /^-----END CERTIFICATE-----$/),
lines = split(trim(filecontent), /[\r\n]/);
let start = false, i;
@ -100,7 +112,10 @@ const methods = {
filecontent += '\n';
system(`mkdir -p ${HP_DIR}/certs`);
writefile(`${HP_DIR}/certs/${filename}.pem`, filecontent);
const certfile = `${HP_DIR}/certs/${filename}.pem`;
writefile(certfile, filecontent);
if (kind === 'private')
system(`chmod 600 ${certfile}`);
system(`rm -f ${tmpcert}`);
return { result: true };
@ -110,10 +125,13 @@ const methods = {
switch (filename) {
case 'client_ca':
case 'server_publickey':
return writeCertificate(filename, false);
return writeCertificate(filename, 'certificate');
break;
case 'client_ech_conf':
return writeCertificate(filename, 'ech');
break;
case 'server_privatekey':
return writeCertificate(filename, true);
return writeCertificate(filename, 'private');
break;
default:
return { result: false, error: 'illegal cerificate filename' };
@ -155,8 +173,8 @@ const methods = {
if (!section || section['.type'] !== 'node')
return { id: section_id, label: fallback_name || section_id, type: null };
const address = trim(section.override_address || section.address || '');
const port = trim(section.override_port || section.port || '');
const address = trim(section.address || '');
const port = trim(section.port || '');
const label = trim(section.label || '') || ((address && port) ? `${address}:${port}` : section_id);
return {
id: section_id,
@ -248,195 +266,6 @@ const methods = {
rp_runtime: (extras && extras.rp_runtime) ? extras.rp_runtime : null
});
const strToBool = (str) => ((str === '1') || null);
const strToInt = (str) => (str != null && trim(sprintf('%s', str)) !== '') ? (int(str) || null) : null;
const strToTime = (str) => (str != null && trim(sprintf('%s', str)) !== '') ? (str + 's') : null;
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) => {
if (type(k) in ['array', 'object'])
push(content, removeBlankAttrs(k));
else if (k !== null && k !== '')
push(content, k);
});
} else
return res;
return content;
}
const parse_port = (strport) => {
if (type(strport) !== 'array' || length(strport) === 0)
return null;
let ports = [];
for (let i = 0; i < length(strport); i++)
push(ports, int(strport[i]));
return ports;
};
const generate_endpoint = (node_cfg) => {
if (type(node_cfg) !== 'object' || length(node_cfg) === 0)
return null;
return {
type: node_cfg.type,
tag: 'cfg-' + node_cfg['.name'] + '-out',
address: node_cfg.wireguard_local_address,
mtu: strToInt(node_cfg.wireguard_mtu),
private_key: node_cfg.wireguard_private_key,
peers: (node_cfg.type === 'wireguard') ? [
{
address: node_cfg.address,
port: strToInt(node_cfg.port),
allowed_ips: [
'0.0.0.0/0',
'::/0'
],
persistent_keepalive_interval: strToInt(node_cfg.wireguard_persistent_keepalive_interval),
public_key: node_cfg.wireguard_peer_public_key,
pre_shared_key: node_cfg.wireguard_pre_shared_key,
reserved: parse_port(node_cfg.wireguard_reserved)
}
] : null,
system: (node_cfg.type === 'wireguard') ? false : null,
tcp_fast_open: strToBool(node_cfg.tcp_fast_open),
tcp_multi_path: strToBool(node_cfg.tcp_multi_path),
udp_fragment: strToBool(node_cfg.udp_fragment)
};
};
const generate_outbound = (node_cfg) => {
if (type(node_cfg) !== 'object' || length(node_cfg) === 0)
return null;
return {
type: node_cfg.type,
tag: 'cfg-' + node_cfg['.name'] + '-out',
server: node_cfg.address,
server_port: strToInt(node_cfg.port),
server_ports: node_cfg.hysteria_hopping_port,
username: (node_cfg.type !== 'ssh') ? node_cfg.username : null,
user: (node_cfg.type === 'ssh') ? node_cfg.username : null,
password: node_cfg.password,
override_address: node_cfg.override_address,
override_port: strToInt(node_cfg.override_port),
proxy_protocol: strToInt(node_cfg.proxy_protocol),
idle_session_check_interval: strToTime(node_cfg.anytls_idle_session_check_interval),
idle_session_timeout: strToTime(node_cfg.anytls_idle_session_timeout),
min_idle_session: strToInt(node_cfg.anytls_min_idle_session),
hop_interval: strToTime(node_cfg.hysteria_hop_interval),
up_mbps: strToInt(node_cfg.hysteria_up_mbps),
down_mbps: strToInt(node_cfg.hysteria_down_mbps),
obfs: node_cfg.hysteria_obfs_type ? {
type: node_cfg.hysteria_obfs_type,
password: node_cfg.hysteria_obfs_password
} : node_cfg.hysteria_obfs_password,
auth: (node_cfg.hysteria_auth_type === 'base64') ? node_cfg.hysteria_auth_payload : null,
auth_str: (node_cfg.hysteria_auth_type === 'string') ? node_cfg.hysteria_auth_payload : null,
recv_window_conn: strToInt(node_cfg.hysteria_recv_window_conn),
recv_window: strToInt(node_cfg.hysteria_revc_window),
disable_mtu_discovery: strToBool(node_cfg.hysteria_disable_mtu_discovery),
method: node_cfg.shadowsocks_encrypt_method,
plugin: node_cfg.shadowsocks_plugin,
plugin_opts: node_cfg.shadowsocks_plugin_opts,
version: (node_cfg.type === 'shadowtls') ? strToInt(node_cfg.shadowtls_version) : ((node_cfg.type === 'socks') ? node_cfg.socks_version : null),
client_version: node_cfg.ssh_client_version,
host_key: node_cfg.ssh_host_key,
host_key_algorithms: node_cfg.ssh_host_key_algo,
private_key: node_cfg.ssh_priv_key,
private_key_passphrase: node_cfg.ssh_priv_key_pp,
uuid: node_cfg.uuid,
congestion_control: node_cfg.tuic_congestion_control,
udp_relay_mode: node_cfg.tuic_udp_relay_mode,
udp_over_stream: strToBool(node_cfg.tuic_udp_over_stream),
zero_rtt_handshake: strToBool(node_cfg.tuic_enable_zero_rtt),
heartbeat: strToTime(node_cfg.tuic_heartbeat),
flow: node_cfg.vless_flow,
alter_id: strToInt(node_cfg.vmess_alterid),
security: node_cfg.vmess_encrypt,
global_padding: strToBool(node_cfg.vmess_global_padding),
authenticated_length: strToBool(node_cfg.vmess_authenticated_length),
packet_encoding: node_cfg.packet_encoding,
multiplex: (node_cfg.multiplex === '1') ? {
enabled: true,
protocol: node_cfg.multiplex_protocol,
max_connections: strToInt(node_cfg.multiplex_max_connections),
min_streams: strToInt(node_cfg.multiplex_min_streams),
max_streams: strToInt(node_cfg.multiplex_max_streams),
padding: strToBool(node_cfg.multiplex_padding),
brutal: (node_cfg.multiplex_brutal === '1') ? {
enabled: true,
up_mbps: strToInt(node_cfg.multiplex_brutal_up),
down_mbps: strToInt(node_cfg.multiplex_brutal_down)
} : null
} : null,
tls: (node_cfg.tls === '1') ? {
enabled: true,
server_name: node_cfg.tls_sni,
insecure: strToBool(node_cfg.tls_insecure),
alpn: node_cfg.tls_alpn,
min_version: node_cfg.tls_min_version,
max_version: node_cfg.tls_max_version,
cipher_suites: node_cfg.tls_cipher_suites,
certificate_path: node_cfg.tls_cert_path,
ech: (node_cfg.tls_ech === '1') ? {
enabled: true,
config: node_cfg.tls_ech_config,
config_path: node_cfg.tls_ech_config_path
} : null,
utls: node_cfg.tls_utls ? {
enabled: true,
fingerprint: node_cfg.tls_utls
} : null,
reality: (node_cfg.tls_reality === '1') ? {
enabled: true,
public_key: node_cfg.tls_reality_public_key,
short_id: node_cfg.tls_reality_short_id
} : null
} : null,
transport: node_cfg.transport ? {
type: node_cfg.transport,
host: node_cfg.http_host || node_cfg.httpupgrade_host,
path: node_cfg.http_path || node_cfg.ws_path,
headers: node_cfg.ws_host ? {
Host: node_cfg.ws_host
} : null,
method: node_cfg.http_method,
max_early_data: strToInt(node_cfg.websocket_early_data),
early_data_header_name: node_cfg.websocket_early_data_header,
service_name: node_cfg.grpc_servicename,
idle_timeout: node_cfg.http_idle_timeout,
ping_timeout: node_cfg.http_ping_timeout,
permit_without_stream: strToBool(node_cfg.grpc_permit_without_stream)
} : null,
udp_over_tcp: (node_cfg.udp_over_tcp === '1') ? {
enabled: true,
version: strToInt(node_cfg.udp_over_tcp_version)
} : null,
tcp_fast_open: strToBool(node_cfg.tcp_fast_open),
tcp_multi_path: strToBool(node_cfg.tcp_multi_path),
udp_fragment: strToBool(node_cfg.udp_fragment)
};
};
const isControllerPortBusy = (port) => {
const hex_port = sprintf('%04X', int(port));
const tcp_v4 = readfile('/proc/net/tcp') || '';
@ -645,7 +474,7 @@ const methods = {
}
};
const rendered_outbound = (node_cfg.type === 'wireguard') ? generate_endpoint(node_cfg) : generate_outbound(node_cfg);
const rendered_outbound = (node_cfg.type === 'wireguard') ? renderEndpoint(node_cfg) : renderOutbound(node_cfg);
if (rendered_outbound && type(rendered_outbound) === 'object')
rendered_outbound.tag = selected_tag;
@ -1104,22 +933,23 @@ const methods = {
}
},
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_get: {
call: function() {
let resources = [];
for (let resource_type in keys(RESOURCE_SOURCES)) {
const version = trim(readfile(`${HP_DIR}/resources/${resource_type}.ver`) || '');
push(resources, {
type: resource_type,
version: version || null,
source: RESOURCE_SOURCES[resource_type]
});
}
return { resources };
}
},
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' };
call: function() {
return { status: system(`${HP_DIR}/scripts/update_resources.sh`) };
}
}
};

View File

@ -1479,6 +1479,7 @@ if is_finded("xray") then
o:value("qq", translate("qq"))
o:value("random", translate("random"))
o:value("randomized", translate("randomized"))
o:value("unsafe", translate("unsafe"))
o:value("", translate("disable"))
o:depends({type = "v2ray", tls = true})
o:depends({type = "v2ray", reality = true})

View File

@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=rblibtorrent
PKG_VERSION:=2.1.0
PKG_VERSION:=2.0.13
PKG_RELEASE:=1
PKG_SOURCE:=libtorrent-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/arvidn/libtorrent/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=5d5b264eb960fafe4a5c935f6c71bc00e02d3dab8872e76dee9dadb27282f912
PKG_HASH:=86207fbe502153568bb4b11d11bb881d6c1388962e852ee0caacf1c07adeb161
PKG_BUILD_DIR:=$(BUILD_DIR)/libtorrent-$(PKG_VERSION)
PKG_LICENSE:=BSD

View File

@ -21,13 +21,13 @@ define Download/geoip
HASH:=b71d1999439dde2de2d2b6844a2befa50c50211ff739785c005ca7c230a17d6a
endef
GEOSITE_VER:=20260711034308
GEOSITE_VER:=20260711103457
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
HASH:=356743c9d1ac82ba9c2ed725cc11e8175d431ca5d327bd1ce457edbba70ffd7c
HASH:=1d09df82a533dd9bcdeaa8ddabb81be4cfa031dea211102ab09d115f984fbe36
endef
GEOSITE_IRAN_VER:=202607060129